PageSourceSearch

https://businessheraldonline.com/js/controllers/carouselController…?v9e91e12689728780f01d6dd9a70eb5c9bae56d64

js businessheraldonline.com collected 2026-09-26 04:42:27 UTC 5,014 bytes, 149 lines download raw bytes

1import { Controller } from "../stimulus.js";
2
3export default class extends Controller {
4  static targets = ["list", "slide", "prev", "next"];
5
6  connect() {
7    if (!this.hasListTarget) return;
8    this._pendingImgHandlers = [];
9    this.whenImagesReady(() => {
10      if (!this.hasListTarget) return;
11      this.setupInfiniteLoop();
12      this.listTarget.style.scrollBehavior = "smooth";
13    });
14  }
15
16  disconnect() {
17    if (this._scrollEndTimer) clearTimeout(this._scrollEndTimer);
18    if (this._scrollEndHandler && this.hasListTarget) {
19      this.listTarget.removeEventListener("scrollend", this._scrollEndHandler);
20      this._scrollEndHandler = null;
21    }
22    (this._pendingImgHandlers || []).forEach(({ img, handler }) => {
23      img.removeEventListener("load", handler);
24      img.removeEventListener("error", handler);
25    });
26    this._pendingImgHandlers = [];
27  }
28
29  // Slide widths depend on image dimensions; measuring scrollWidth before
30  // images load yields a wrong loop offset. Wait for in-list <img>s first.
31  whenImagesReady(cb) {
32    const imgs = Array.from(this.listTarget.querySelectorAll("img"));
33    const pending = imgs.filter((img) => !img.complete);
34    if (!pending.length) return cb();
35
36    let remaining = pending.length;
37    pending.forEach((img) => {
38      const handler = () => {
39        img.removeEventListener("load", handler);
40        img.removeEventListener("error", handler);
41        if (--remaining === 0) cb();
42      };
43      this._pendingImgHandlers.push({ img, handler });
44      img.addEventListener("load", handler);
45      img.addEventListener("error", handler);
46    });
47  }
48
49  setupInfiniteLoop() {
50    if (this.listTarget.querySelector('[data-clone="true"]')) return;
51    const originals = Array.from(this.listTarget.children);
52    if (!originals.length) return;
53
54    const pre = document.createDocumentFragment();
55    const post = document.createDocumentFragment();
56    originals.forEach((slide) => {
57      pre.appendChild(this.buildClone(slide));
58      post.appendChild(this.buildClone(slide));
59    });
60    this.listTarget.insertBefore(pre, this.listTarget.firstChild);
61    this.listTarget.appendChild(post);
62
63    // park on the originals (middle third) so scrolling has clones on both sides
64    this.listTarget.scrollLeft = this.loopWidth;
65  }
66
67  buildClone(slide) {
68    const clone = slide.cloneNode(true);
69    clone.setAttribute("aria-hidden", "true");
70    clone.dataset.clone = "true";
71    clone.removeAttribute("id");
72    clone.querySelectorAll("[id]").forEach((el) => el.removeAttribute("id"));
73    clone.querySelectorAll("[for]").forEach((el) => el.removeAttribute("for"));
74    clone
75      .querySelectorAll("[aria-labelledby]")
76      .forEach((el) => el.removeAttribute("aria-labelledby"));
77    clone
78      .querySelectorAll("[aria-controls]")
79      .forEach((el) => el.removeAttribute("aria-controls"));
80    return clone;
81  }
82
83  // one period = width of the originals strip including its trailing gap.
84  // With pre + originals + post, scrollWidth = 3 periods - one trailing gap.
85  get loopWidth() {
86    const gap = this.getGapPx(this.listTarget);
87    return (this.listTarget.scrollWidth + gap) / 3;
88  }
89
90  next() {
91    this.listTarget.scrollBy({ left: this.stepSize(), behavior: "smooth" });
92    this.afterScroll(() => this.rebaseScroll());
93  }
94
95  prev() {
96    this.listTarget.scrollBy({ left: -this.stepSize(), behavior: "smooth" });
97    this.afterScroll(() => this.rebaseScroll());
98  }
99
100  // after any scroll, keep scrollLeft anchored on the middle (originals) copy
101  rebaseScroll() {
102    const width = this.loopWidth;
103    if (!width) return;
104    if (this.listTarget.scrollLeft >= 2 * width) {
105      this.snapBy(-width);
106    } else if (this.listTarget.scrollLeft < width) {
107      this.snapBy(width);
108    }
109  }
110
111  snapBy(delta) {
112    const prev = this.listTarget.style.scrollBehavior;
113    this.listTarget.style.scrollBehavior = "auto";
114    this.listTarget.scrollLeft += delta;
115    this.listTarget.style.scrollBehavior = prev;
116  }
117
118  afterScroll(cb) {
119    if (this._scrollEndTimer) clearTimeout(this._scrollEndTimer);
120    if (this._scrollEndHandler) {
121      this.listTarget.removeEventListener("scrollend", this._scrollEndHandler);
122    }
123    const handler = () => {
124      this.listTarget.removeEventListener("scrollend", handler);
125      clearTimeout(this._scrollEndTimer);
126      this._scrollEndHandler = null;
127      cb();
128    };
129    this._scrollEndHandler = handler;
130    this.listTarget.addEventListener("scrollend", handler, { once: true });
131    // fallback for browsers without scrollend
132    this._scrollEndTimer = setTimeout(handler, 600);
133  }
134
135  stepSize() {
136    const first = this.slideTargets[0];
137    if (!first) return this.listTarget.clientWidth * 0.9;
138
139    const rect = first.getBoundingClientRect();
140    const gap = this.getGapPx(this.listTarget);
141    return rect.width + gap;
142  }
143
144  getGapPx(el) {
145    const style = getComputedStyle(el);
146    const gap = parseInt(style.gap || style.columnGap || "0", 10);
147    return isNaN(gap) ? 0 : gap;
148  }
149}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.