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.