PageSourceSearch

https://boiseschools.org/js/components/pagination.js

js boiseschools.org collected 2026-09-24 09:35:12 UTC 7,788 bytes, 204 lines download raw bytes

1/**
2 * Generic Pagination Controller
3 * - Initializes on elements with [data-js-pagination]
4 * - Manages active/disabled UI state
5 * - Exposes API via window.SS.pagination
6 * - Emits 'ss:pagination:changed' CustomEvent on container when index changes (unless silent)
7 */
8
9(() => {
10    const ns = (window.SS = window.SS || {});
11    const registry = (ns.paginationRegistry = ns.paginationRegistry || new Map());
12
13    let autoIdCounter = 1;
14
15    class PaginationController {
16        constructor(container) {
17            this.container = container;
18            this.$container = $(container);
19            this.totalPages = this._computeTotalPages();
20            this.currentIndex = this._initialIndex();
21            this.maxVisible = this._maxVisible();
22            this._assignIdIfMissing();
23            this._bindClicks();
24            this.updateUi();
25        }
26
27        get id() {
28            return this.container.getAttribute('data-pagination-id');
29        }
30
31        setIndex(index, options = { silent: false }) {
32            const clampedIndex = Math.max(0, Math.min(index, this.totalPages - 1));
33            if (Number.isNaN(clampedIndex) || clampedIndex === this.currentIndex) {
34                return;
35            }
36            this.currentIndex = clampedIndex;
37            this.updateUi();
38            if (!options.silent) {
39                this._emitChanged();
40            }
41        }
42
43        updateUi() {
44            const index = this.currentIndex;
45            const total = this.totalPages;
46
47            // Update active state for numbered links
48            const $numbered = this.$container.find('li');
49            $numbered.removeClass('active');
50            // Find numbered page anchor matching the index (exclude fastback_link)
51            const $numberAnchor = this.$container.find(`a[data-sli
51de="${index}"]:not(.fastback_link)`);
52            if ($numberAnchor.length) {
53                $numberAnchor.closest('li').addClass('active');
54            }
55
56            // Update which numbers are visible (cap to maxVisible)
57            this._updateNumberVisibility();
58
59            // Update disabled states for nav links
60            const atStart = index <= 0;
61            const atEnd = index >= total - 1;
62
63            this._setDisabled(this.$container.find('.fastback_link').closest('li'), atStart);
64            this._setDisabled(this.$container.find('.prev_link').closest('li'), atStart);
65            this._setDisabled(this.$container.find('.next_link').closest('li'), atEnd);
66            this._setDisabled(this.$container.find('.fastforward_link').closest('li'), atEnd);
67        }
68
69        _setDisabled($li, disabled) {
70            if (!$li || !$li.length) return;
71            if (disabled) {
72                $li.addClass('disabled').attr('aria-disabled', 'true');
73                $li.find('a').attr('tabindex', '0');
74            } else {
75                $li.removeClass('disabled').removeAttr('aria-disabled');
76                $li.find('a').attr('tabindex', '0');
77            }
78        }
79
80        _computeTotalPages() {
81            const fromAttr = parseInt(this.container.getAttribute('data-total-pages') || '', 10);
82            if (Number.isInteger(fromAttr) && fromAttr > 0) return fromAttr;
83            // Fallback: count numbered anchors
84            const numbered = this.container.querySelectorAll('a[data-slide]:not([data-slide="prev"]):not([data-slide="next"]):not([data-slide="last"]):not(.fastback_link)');
85            return numbered.length || 0;
86        }
87
88        _initialIndex() {
89            const fromAttr = parseInt(this.container.getAttribute('data-initial-index') || '', 10);
90            if (Number.isInteger(fromAttr)) return fromAttr;
91            const activeAnchor = this.container.querySelector('li.active a[data-slide]');
92            if (activeAnchor) {
93                const n = parseInt(activeAnchor.getAttribute('data-slide') || '0', 10);
94                return Number.isInteger(n) ? n : 0;
95            }
96            return 0;
97        }
98
99        _maxVisible() {
100            const fromAttr = parseInt(this.container.getAttribute('data-max-visible') || '', 10);
101            if (Number.isInteger(fromAttr) && fromAttr > 0) return fromAttr;
102            return 8;
103        }
104
105        _updateNumberVisibility() {
106            const total = this.totalPages;
107            const maxVisible = this.maxVisible;
108            const index = this.currentIndex;
109            const $numberAnchors = this.$container.find('a[data-slide]').filter(function() {
110                const $el = $(this);
111                const v = $el.data('slide');
112                return typeof v === 'number' && !$el.hasClass('fastback_link');
113            });
114
115            if (total <= maxVisible) {
116                $numberAnchors.closest('li').show();
117                return;
118            }
119
120            let start = Math.max(0, index - Math.floor(maxVisible / 2));
121            if (start + maxVisible > total) {
122                start = Math.max(0, total - maxVisible);
123            }
124            const end = Math.min(total - 1, start + maxVisible - 1);
125
126            $numberAnchors.each(function() {
127                const slide = $(this).data('slide');
128                const $li = $(this).closest('li');
129                if (slide >= start && slide <= end) {
130                    $li.show();
131                } else {
132                    $li.hide();
133                }
134            });
135        }
136
137        _assignIdIfMissing() {
138            if (!this.container.hasAttribute('data-pagination-id')) {
139                this.container.setAttribute('data-pagination-id', `pg-${autoIdCounter++}`);
140            }
141        }
142
143        _bindClicks() {
144            this.$container.on('click', 'a[data-slide]', (e) => {
145                e.preventDefault();
146                e.stopPropagation();
147                const $a = $(e.currentTarget);
148                const slideData = $a.data('slide');
149                if (typeof slideData === 'number') {
150                    this.setIndex(slideData);
151                } else if (slideData === 'last') {
152                    this.setIndex(this.totalPages - 1);
153                } else if (slideData === 0) {
154                    this.setIndex(0);
155                } else if (slideData === 'prev') {
156                    this.setIndex(this.currentIndex - 1);
157                } else if (slideData === 'next') {
158                    this.setIndex(this.currentIndex + 1);
159                }
160            });
161        }
162
163        _emitChanged() {
164            const ev = new CustomEvent('ss:pagination:changed', {
165                bubbles: true,
166                detail: { index: this.currentIndex, controller: this }
167            });
168            this.container.dispatchEvent(ev);
169        }
170    }
171
172    // Public API
173    ns.pagination = ns.pagination || {};
174    ns.pagination.getController = function(idOrElement) {
175        const el = typeof idOrElement === 'string'
176            ? document.querySelector(`[data-pagination-id="${idOrElement}"]`) || document.querySelector(idOrElement)
177            : idOrElement;
178        if (!el) return null;
179        return registry.get(el) || null;
180    };
181    ns.pagination.setIndex = function(idOrElement, index, options) {
182        const controller = ns.pagination.getController(idOrElement);
183        if (controller) controller.setIndex(index, options || {});
184    };
185
186    // Initialize
187    const initAll = () => {
188        const nodes = document.querySelectorAll('[data-js-pagination]');
189        nodes.forEach(node => {
190            if (!registry.has(node)) {
191                const controller = new PaginationController(node);
192                registry.set(node, controller);
193            }
194        });
195    };
196
197    if (document.readyState === 'loading') {
198        document.addEventListener('DOMContentLoaded', initAll);
199    } else {
200        initAll();
201    }
202})();
203
204

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.