1/** 2 * SwipeHelper 3 * Acts on a container displaying items overflowing horizontally or vertically. 4 * User can swipe / scroll normally or use click-handlers delivered by this class. 5 */ 6 7if (!window.Dac) window.Dac = {}; 8// eslint-disable-next-line prefer-const 9let { Dac } = window; 10Dac.SwipeHelper = class { 11 12 constructor(container, options) { 13 this.debug = window.location.toString().includes('debug='); 14 this.log('Creating instance ...'); 15 this.settings = { 16 isHorizontal: options.isHorizontal, 17 css: { 18 hidden: 'nav-hidden' 19 }, 20 navWidth: 0, 21 preloadNextImage: !!options.preloadNextImage, 22 intersectionObserver: { 23 threshold: typeof options.threshold === 'undefined' ? 0.5 : options.threshold 24 }, 25 itemsSelector: options.itemsSelector 26 }; 27 this.callbacks = { 28 navUpdate: [] 29 }; 30 const navItems = options.navItemsSelector ? container.querySelector(options.navItemsSelector) : null; 31 const previewItems = options.previewItemsSelector ? container.querySelector(options.previewItemsSelector) : null; 32 this.elements = { 33 nav: { 34 forward: container.querySelector(options.navItems.forwardSelector), 35 backward: container.querySelector(options.navItems.backwardSelector) 36 }, 37 itemsContainer: this.settings.isHorizontal ? container.querySelector(options.itemsContainerSelector) : document.querySelector(options.itemsContainerSelector), 38 items: container.querySelectorAll(this.settings.itemsSelector), 39 navItems: navItems ? [...navItems.children] : null, 40 previewItems: previewItems ? [...previewItems.children] : null 41 }; 42 this.noIndexList = []; 43 this.domIndex = null; 44 45 if (options.noIndexItemsSelector) { 46 const noIndexElements = container.querySelectorAll(options.noIndexItemsSelector); 47 if (noIndexElements && noIndexElements.length) { 48 this.log(`Will exclude ${ noIndexElements.length } elements from indexing ...`); 49 for (const el of noIndexElements) { 50 const index = Array.from(this.elements.items).indexOf(el); 51 if (index !== -1) { 52 this.noIndexList.push(index); 53 this.log(` - Excluding index ${ index }`); 54 } 55 } 56 this.log(`Did exclude ${ this.noIndexList.length } elements from indexing ...`); 57 } 58 } 59 60 if ( 61 !this.validateElement(this.elements.itemsContainer) 62 || !this.validateElement(this.elements.items) 63 ) { 64 this.log('Missing required dom-element(s).', true); 65 return; 66 } 67 if (this.elements.items.length < 2) { 68 this.log('To few elements. No need for the helper.', true); 69 return; 70 } 71 this.items = [...this.elements.items].map((el) => ({ visible: false, element: el })); 72 this.index = null; 73 this.observer = null; 74 this.observeElements(this.elements.itemsContainer, this.elements.items); 75 this.setupNavigation(); 76 this.autoScrollInterval = null; 77 this.autoScroll = options.autoScroll; 78 this.elementWidth = this.items[0].element.scrollWidth; 79 this.currentLeft = 0; 80 this.isAutoscrolling = false; 81 this.preloadedIndexes = []; 82 this.registerAutoscroll(); 83 } 84 85 addNavUpdate(callback) { 86 this.log('Adding navigation update callback ...'); 87 this.callbacks.navUpdate.push(callback); 88 } 89 90 reflow() { 91 this.log(`Will reflow items`); 92 this.unobserve(this.elements.items); 93 this.elements.items = this.elements.itemsContainer.querySelectorAll(this.settings.itemsSelector); 94 this.items = [...this.elements.items].map((el) => ({ visible: false, element: el })); 95 this.observeElements(this.elements.itemsContainer, this.elements.items); 96 } 97 98 log(msg, isCritical = false) { 99 if (isCritical) { 100 console.warn('[SwipeHelper]', msg); 101 } 102 if (!this.debug) { return; } 103 console.log('[SwipeHelper]', msg); 104 } 105 106 // NodeList or element 107 validateElement(element) { 108 return element && (element.length || element.nodeType === 1); 109 } 110 111 // Only use autoscrolling when element is visible 112 registerAutoscroll() { 113 if (!this.autoScroll || !this.autoScroll.enabled) { 114 return; 115 } 116 if (!this.autoScroll.interval) { 117 this.autoScroll.interval = 4000; 118 } 119 const options = { 120 root: null, 121 rootMargin: '0px', 122 threshold: 0 123 }; 124 const observer = new IntersectionObserver((entries, theObserver) => { 125 if (entries[0].isIntersecting) { 126 this.startAutoScroll(); 127 } else { 128 this.stopAutoScroll(); 129 } 130 }, options); 131 observer.observe(this.elements.itemsContainer); 132 } 133 134 startAutoScroll() { 135 if (this.isAutoscrolling || !this.settings.isHorizontal) { 136 return; 137 } 138 this.isAutoscrolling = true; 139 this.log('Starting autoscroll ...'); 140 141 this.autoScrollInterval = setInterval(() => { 142 this.autoScroll.scrolling = true; 143 144 if (this.autoScroll.reset) { 145 this.elements.itemsContainer.scrollTo({ 146 top: 0, 147 left: 0, 148 behavior: 'smooth' 149 }); 150 this.autoScroll.reset = false;
151 } else { 152 this.elements.itemsContainer.scrollTo({ 153 top: 0, 154 left: this.elements.itemsContainer.scrollLeft + this.elementWidth, 155 behavior: 'smooth' 156 }); 157 } 158 setTimeout(() => { 159 this.autoScroll.scrolling = false; 160 }, 1000); 161 }, parseInt(this.autoScroll.interval, 10)); 162 163 const scrolled = () => { 164 if (!this.autoScroll.scrolling) { 165 this.log('Will disable autoscroll'); 166 this.stopAutoScroll(); 167 this.elements.itemsContainer.removeEventListener('scroll', scrolled); 168 } 169 }; 170 171 this.elements.itemsContainer.addEventListener('scroll', scrolled); 172 } 173 174 stopAutoScroll() { 175 if (!this.isAutoscrolling) { 176 return; 177 } 178 this.log('Stopping autoscroll ...'); 179 clearInterval(this.autoScrollInterval); 180 this.autoScrollInterval = null; 181 this.isAutoscrolling = false; 182 } 183 184 setupNavigation() { 185 if (this.elements.nav.forward && this.elements.nav.backward) { 186 this.log(`Will register events for navigation.`); 187 this.settings.navWidth = this.elements.nav.backward.offsetWidth; 188 this.elements.nav.backward.addEventListener('click', (event) => { 189 event.preventDefault(); 190 event.stopPropagation(); 191 this.navigate(-1); 192 }, false); 193 this.elements.nav.forward.addEventListener('click', (event) => { 194 event.preventDefault(); 195 event.stopPropagation(); 196 this.navigate(1); 197 }, false); 198 } 199 if (this.elements.navItems) { 200 for (const item of this.elements.navItems) { 201 const index = this.elements.navItems.indexOf(item); 202 item.addEventListener('click', (event) => { 203 this.navigateToIndex(index, 'auto'); 204 }, false); 205 } 206 } 207 if (this.elements.previewItems) { 208 for (const item of this.elements.previewItems) { 209 const index = this.elements.previewItems.indexOf(item); 210 item.addEventListener('click', (event) => { 211 this.navigateToIndex(index, 'auto'); 212 }, false); 213 } 214 } 215 } 216 217 navigateToIndex(index, behavior) { 218 this.log(`Navigate to index: ${ index }`); 219 if (!this.items[index]) { 220 return; 221 } 222 this.navigateToItem(this.items[index], behavior); 223 } 224 225 navigate(steps) { 226 const visibleItems = this.items.filter((itm) => itm.visible); 227 if (!visibleItems.length) {
228 this.log('Cannot navigate. Did not find any visible items ...', true); 229 return; 230 } 231 // Calculate index based on the first visible item: 232 const index = this.items.indexOf(visibleItems.shift()) + steps; 233 if (index >= 0 && index < this.items.length) { 234 this.navigateToItem(this.items[index]); 235 } 236 } 237 238 navigateToItem(item, behavior = 'smooth') { 239 if (this.settings.isHorizontal) { 240 const leftPos = Math.max(0, item.element.offsetLeft - this.settings.navWidth); 241 this.elements.itemsContainer.scrollTo({ 242 top: 0, 243 left: leftPos, 244 behavior 245 }); 246 } else { 247 if (this.elements.itemsContainer === document.body) { 248 window.scrollTo({ 249 top: item.element.offsetTop, 250 left: 0, 251 behavior 252 }); 253 } else { 254 this.elements.itemsContainer.scrollTo({ 255 top: item.element.offsetTop, 256 left: 0, 257 behavior 258 }); 259 } 260 } 261 } 262 263 // this.noIndexList holds a list of items that should not be counted in the public index. 264 internalToPublicIndex(internalIndex, isForward = true) { 265 let result = internalIndex; 266 if (this.noIndexList.length) { 267 let offset = 0; 268 for (const noIndex of this.noIndexList) { 269 if ((!isForward && noIndex < internalIndex) || (isForward && noIndex <= internalIndex)) { 270 offset++; 271 } 272 } 273 result -= offset; 274 } 275 return result; 276 } 277 278 publicToInternalIndex(publicIndex) { 279 let result = publicIndex; 280 if (this.noIndexList.length) { 281 let offset = 0; 282 for (const noIndex of this.noIndexList) { 283 if (noIndex <= result + offset) { 284 offset++; 285 } 286 } 287 result += offset; 288 } 289 return result; 290 } 291 292 observeElements(root, items) { 293 this.log(`Will register IntersectionObserver for ${ items.length } items`); 294 const options = { 295 root: this.settings.isHorizontal ? root : null, 296 rootMargin: '0px', 297 threshold: this.settings.intersectionObserver.threshold 298 }; 299 this.observer = new IntersectionObserver((entries, theObserver) => { 300 const touchedIndexes = []; 301 let visibleIndex = null; 302 for (const entry of entries) { 303 const index = Array.from(entry.target.parentNode.querySelectorAll(this.settings.itemsSelector)).indexOf(entry.target); 304 if (index === -1) { 305 this.unobserve(items); 306 return; 307 } 308 this.items[index].visible = entry.isIntersecting; 309 touchedIndexes.push(index); 310 if (this.items[index].visible) { 311 visibleIndex = index; 312 } 313 if (this.settings.preloadNextImage && entry.isIntersecting && this.items[index + 1] && !this.preloadedIndexes.includes(index + 1)) { 314 this.preloadAtIndex(index + 1); 315 } 316 this.log(`IntersectionObserver triggered at index ${ index }. Setting visibility to ${ this.items[index].visible }.`); 317 } 318 if (visibleIndex !== null) { 319 const direction = visibleIndex >= this.domIndex ? 'next' : 'prev'; 320 let noIndexDistance = null; 321 // Find distance to next noIndex item. Use direction to decide if we should look forward or backward: 322 if (this.noIndexList.length) { 323 noIndexDistance = this.noIndexList.map((noIndex) => noIndex - visibleIndex).filter((dist) => (direction === 'next' ? dist >= 0 : dist <= 0)); 324 if (noIndexDistance.length) { 325 noIndexDistance = direction === 'next' ? Math.min(...noIndexDistance) : Math.max(...noIndexDistance); 326 } else { 327 noIndexDistance = null; 328 } 329 } 330 // Set noIndexDistance to a positive int: 331 if (noIndexDistance !== null && noIndexDistance < 0) { 332 noIndexDistance = Math.abs(noIndexDistance); 333 } 334 335 this.updateNavItems(this.internalToPublicIndex(visibleIndex, visibleIndex > this.index), { 336 originalIndex: visibleIndex, 337 direction, 338 noIndexDistance 339 }); 340 } 341 if (touchedIndexes.includes(0) || touchedIndexes.includes(this.items.length - 1)) { 342 this.updateGui(); 343 if (this.autoScroll && this.autoScroll.enabled && this.items[this.items.length - 1].visible) { 344 this.autoScroll.reset = true; 345 } 346 } 347 this.domIndex = visibleIndex; 348 }, options); 349 for (const item of items) { 350 this.observer.observe(item); 351 } 352 } 353 354 unobserve(items) { 355 this.log(`Will unobserve ${ items.length } items`); 356 for (const item of items) { 357 this.observer.unobserve(item); 358 } 359 } 360 361 // Remove "loading"-attribute to force browser to load image. 362 preloadAtIndex(index) { 363 this.preloadedIndexes.push(index); 364 if (!this.items[index] || !this.items[index].element) { return; } 365 const img = this.items[index].element.querySelector('img'); 366 if (img) { 367 this.log(`Will preload image at index ${ index }`); 368 img.removeAttribute('loading'); 369 } 370 } 371 372 // Update nav-elements based on visibility of first / last item 373 updateGui() { 374 if (this.elements.nav.forward && this.elements.nav.backward) { 375 this.log('Updating navigation-GUI ...'); 376 if (this.items[0].visible) { 377 this.elements.nav.backward.classList.add(this.settings.css.hidden); 378 } else { 379 this.elements.nav.backward.classList.remove(this.settings.css.hidden); 380 } 381 if (this.items[this.items.length - 1].visible) { 382 this.elements.nav.forward.classList.add(this.settings.css.hidden); 383 } else { 384 this.elements.nav.forward.classList.remove(this.settings.css.hidden); 385 } 386 } 387 } 388 389 updateNavItems(index, { originalIndex, direction, noIndexDistance }) { 390 const selector = 'selected'; 391 if (this.elements.navItems) { 392 if (this.elements.navItems[this.index]) { 393 this.elements.navItems[this.index].classList.remove(selector); 394 } 395 if (this.elements.navItems[index]) { 396 this.elements.navItems[index].classList.add(selector); 397 } 398 } 399 if (this.elements.previewItems) { 400 if (this.elements.previewItems[this.index]) { 401 this.elements.previewItems[this.index].classList.remove(selector); 402 } 403 if (this.elements.previewItems[index]) { 404 this.elements.previewItems[index].classList.add(selector); 405 } 406 } 407 this.index = index; 408 409 for (const fn of this.callbacks.navUpdate) { 410 fn(this.index, { 411 isNoIndex: this.noIndexList.includes(originalIndex), 412 direction, 413 domIndex: originalIndex, 414 noIndexDistance 415 }); 416 } 417 418 } 419 420};
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.