1(function () { 2 'use strict'; 3 4 const selector = '[data-knowol-before-after]'; 5 const reducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; 6 const clamp = (value, min, max) => Math.max(min, Math.min(max, value)); 7 const bool = (value) => value === true || value === 'true' || value === '1' || value === 'yes'; 8 9 class KnowolBeforeAfter { 10 constructor(root) { 11 this.root = root; 12 this.stage = root.querySelector('.kabaf-stage'); 13 this.handle = root.querySelector('.kabaf-handle'); 14 this.beforeImg = root.querySelector('.kabaf-img-before'); 15 this.afterImg = root.querySelector('.kabaf-img-after'); 16 this.orientation = root.dataset.orientation || 'horizontal'; 17 this.mode = root.dataset.mode || 'slider'; 18 this.start = clamp(parseFloat(root.dataset.start || '50'), 0, 100); 19 this.position = this.start; 20 this.dragging = false; 21 this.hover = bool(root.dataset.hover); 22 this.click = bool(root.dataset.click); 23 this.keyboard = bool(root.dataset.keyboard); 24 this.autoplayRequested = bool(root.dataset.autoplay) && !reducedMotion; 25 this.autoplaySpeed = Math.max(1000, parseInt(root.dataset.autoplaySpeed || '4500', 10)); 26 this.autoplayFrame = null; 27 this.autoplayLast = 0; 28 this.autoplayDirection = 1; 29 this.matchSize = bool(root.dataset.matchSize); 30 this.inView = true; 31 this.resizeObserver = null; 32 this.lastLayerHeight = 0; 33 34 this.setPosition(this.position, false); 35 this.syncLayerSizing(); 36 this.syncImageBox(); 37 this.bind(); 38 this.observeVisibility(); 39 40 if (this.autoplayRequested) { 41 this.startAutoplay(); 42 } 43 44 root.__knowolBeforeAfter = this; 45 this.dispatch('ready'); 46 } 47 48 bind() { 49 if (!this.stage || !this.handle) { 50 return; 51 } 52 53 this.stage.addEventListener('pointerdown', (event) => this.onPointerDown(event)); 54 this.stage.addEventListener('pointermove', (event) => this.onPointerMove(event)); 55 this.stage.addEventListener('pointerup', (event) => this.onPointerEnd(event)); 56 this.stage.addEventListener('pointercancel', (event) => this.onPointerEnd(event)); 57 this.stage.addEventListener('pointerleave', () => this.root.classList.remove('is-lens-visible')); 58 this.stage.addEventListener('click', (event) => this.onClick(event)); 59 this.handle.addEventListener('keydown', (event) => this.onKeyDown(event)); 60 this.root.addEventListener('click', (event) => this.onToolClick(event)); 61 62 document.addEventListener('fullscreenchange', () => { 63 this.root.classList.toggle('is-fullscreen', document.fullscreenElement === this.root); 64 }); 65 } 66 67 observeVisibility() { 68 if (!('IntersectionObserver' in window)) { 69 return; 70 } 71 72 const observer = new IntersectionObserver((entries) => { 73 entries.forEach((entry) => { 74 if (entry.target !== this.root) { 75 return; 76 } 77 78 this.inView = entry.isIntersecting; 79 if (!this.inView) { 80 this.stopAutoplay(false); 81 } else if (this.autoplayRequested) { 82 this.startAutoplay(); 83 } 84 }); 85 }, { threshold: 0.1 }); 86 87 observer.observe(this.root); 88 } 89 90 onPointerDown(event) { 91 if (event.button && event.button !== 0) { 92 return; 93 } 94 95 if (event.target.closest('.kabaf-toolbar')) { 96 return; 97 } 98 99 event.preventDefault(); 100 this.dragging = true; 101 this.root.classList.add('is-dragging'); 102 this.stopAutoplay(true); 103 this.stage.setPointerCapture(event.pointerId); 104 this.updateFromEvent(event); 105 } 106 107 onPointerMove(event) { 108 if (this.dragging || this.hover) { 109 this.updateFromEvent(event); 110 } 111 112 } 113 114 onPointerEnd(event) { 115 if (!this.dragging) { 116 return; 117 } 118 119 this.updateFromEvent(event); 120 if (this.stage.hasPointerCapture && this.stage.hasPointerCapture(event.pointerId)) { 121 this.stage.releasePointerCapture(event.pointerId); 122 } 123 } 124 125 onClick(event) { 126 if (!this.click || event.target.closest('button, a')) { 127 return; 128 } 129 130 this.updateFromEvent(event); 131 } 132 133 onKeyDown(event) { 134 if (!this.keyboard) { 135 return; 136 } 137 138 const horizontal = this.orientation !== 'vertical'; 139 const small = event.shiftKey ? 10 : 2; 140 let next = this.position; 141 142 if ((horizontal && event.key === 'ArrowLeft') || (!horizontal && event.key === 'ArrowUp')) { 143 next -= small; 144 } else if ((horizontal && event.key === 'ArrowRight') || (!horizontal && event.key === 'ArrowDown')) { 145 next += small; 146 } else if (event.key === 'PageDown') { 147 next -= 10; 148 } else if (event.key === 'PageUp') { 149 next += 10; 150 } else if (event.key === 'Home') { 151 next = 0; 152 } else if (event.key === 'End') { 153 next = 100; 154 } else { 155 return; 156 } 157 158 event.preventDefault(); 159 this.stopAutoplay(true); 160 this.setPosition(next); 161 } 162 163 onToolClick(event) { 164 const button = event.target.closest('[data-kabaf-action]'); 165 if (!button || !this.root.contains(button)) { 166 return; 167 } 168 169 const action = button.dataset.kabafAction; 170
171 if (action === 'reset') { 172 this.stopAutoplay(true); 173 this.setPosition(this.start); 174 } 175 176 if (action === 'before') { 177 this.stopAutoplay(true); 178 this.setPosition(100); 179 } 180 181 if (action === 'after') { 182 this.stopAutoplay(true); 183 this.setPosition(0); 184 } 185 186 if (action === 'colorize') { 187 this.stopAutoplay(true); 188 this.setPosition(0); 189 } 190 191 if (action === 'labels') { 192 this.root.classList.toggle('kabaf-hide-labels'); 193 button.setAttribute('aria-pressed', this.root.classList.contains('kabaf-hide-labels') ? 'false' : 'true'); 194 } 195 196 if (action === 'play') { 197 if (this.autoplayFrame) { 198 this.autoplayRequested = false; 199 this.stopAutoplay(true); 200 } else { 201 this.autoplayRequested = true; 202 this.startAutoplay(); 203 } 204 } 205 206 if (action === 'fullscreen') { 207 this.toggleFullscreen(); 208 } 209 } 210 211 updateFromEvent(event) { 212 this.setPosition(this.pointToValue(event)); 213 214 if (event.type === 'pointerup' || event.type === 'pointercancel') { 215 this.dragging = false; 216 this.root.classList.remove('is-dragging'); 217 } 218 } 219 220 pointToValue(event) { 221 const rect = this.stage.getBoundingClientRect(); 222 223 if (this.orientation === 'vertical') { 224 return clamp(((event.clientY - rect.top) / rect.height) * 100, 0, 100); 225 } 226 227 return clamp(((event.clientX - rect.left) / rect.width) * 100, 0, 100); 228 } 229 230 setPosition(value, emit = true) { 231 this.position = clamp(parseFloat(value), 0, 100); 232 const percent = `${this.position.toFixed(2)}%`; 233 this.root.style.setProperty('--kabaf-position', percent); 234 this.root.style.setProperty('--kabaf-decimal', (this.position / 100).toFixed(4)); 235 236 if (this.handle) { 237 this.handle.setAttribute('aria-valuenow', Math.round(this.position)); 238 } 239 240 if (emit) { 241 this.dispatch('change', { position: this.position }); 242 } 243 } 244 245 startAutoplay() { 246 if (this.autoplayFrame || reducedMotion || !this.inView) { 247 return; 248 } 249 250 this.root.classList.add('is-playing'); 251 this.setPlayButton(true); 252 this.autoplayLast = 0; 253 254 const tick = (timestamp) => { 255 if (!this.autoplayFrame) { 256 return; 257 } 258 259 if (!this.autoplayLast) { 260 this.autoplayLast = timestamp; 261 } 262 263 const delta = timestamp - this.autoplayLast; 264 this.autoplayLast = timestamp; 265 let next = this.position + (delta / this.autoplaySpeed) * 100 * this.autoplayDirection; 266 267 if (next >= 100) { 268 next = 100; 269 this.autoplayDirection = -1; 270 } 271 272 if (next <= 0) { 273 next = 0; 274 this.autoplayDirection = 1; 275 } 276 277 this.setPosition(next, false); 278 this.autoplayFrame = window.requestAnimationFrame(tick); 279 }; 280 281 this.autoplayFrame = window.requestAnimationFrame(tick); 282 } 283 284 stopAutoplay(userInitiated) { 285 if (this.autoplayFrame) { 286 window.cancelAnimationFrame(this.autoplayFrame); 287 this.autoplayFrame = null; 288 } 289 290 if (userInitiated) { 291 this.autoplayRequested = false; 292 } 293 294 this.root.classList.remove('is-playing'); 295 this.setPlayButton(false); 296 } 297 298 setPlayButton(playing) { 299 const button = this.root.querySelector('[data-kabaf-action="play"]'); 300 if (!button) { 301 return; 302 } 303 304 button.setAttribute('aria-pressed', playing ? 'true' : 'false'); 305 button.textContent = playing ? 'Pause' : 'Play'; 306 } 307 308 syncLayerSizing() { 309 if (!this.stage || this.stage.classList.contains('kabaf-stage--fixed') || !this.beforeImg || !this.afterImg) { 310 return; 311 } 312 313 Promise.all([this.imageReady(this.beforeImg), this.imageReady(this.afterImg)]).then(() => { 314 this.stage.classList.add('kabaf-stage--layered'); 315 this.updateLayerHeight(); 316 317 if ('ResizeObserver' in window && !this.resizeObserver) { 318 this.resizeObserver = new ResizeObserver(() => this.updateLayerHeight()); 319 this.resizeObserver.observe(this.root); 320 } else { 321 window.addEventListener('resize', () => this.updateLayerHeight(), { passive: true }); 322 } 323 }); 324 } 325 326 updateLayerHeight() { 327 if (!this.stage || !this.beforeImg || !this.beforeImg.naturalWidth || !this.beforeImg.naturalHeight) { 328 return; 329 } 330 331 const width = this.stage.getBoundingClientRect().width; 332 if (!width) { 333 return; 334 } 335 336 const height = width * (this.beforeImg.naturalHeight / this.beforeImg.naturalWi
336dth); 337 if (Math.abs(height - this.lastLayerHeight) < 0.5) { 338 return; 339 } 340 341 this.lastLayerHeight = height; 342 this.stage.style.setProperty('--kabaf-stage-height', `${height}px`); 343 } 344 345 syncImageBox() { 346 if (!this.matchSize || !this.stage || !this.beforeImg || !this.afterImg) { 347 return; 348 } 349 350 Promise.all([this.imageReady(this.beforeImg), this.imageReady(this.afterImg)]).then(() => { 351 if (this.beforeImg.naturalWidth && this.beforeImg.naturalHeight) { 352 this.stage.style.setProperty('--kabaf-matched-aspect', `${this.beforeImg.naturalWidth} / ${this.beforeImg.naturalHeight}`); 353 this.stage.classList.add('kabaf-stage--matched'); 354 this.updateLayerHeight(); 355 } 356 357 this.dispatch('matched'); 358 }); 359 } 360 361 imageReady(image) { 362 if (image.complete && image.naturalWidth) { 363 return Promise.resolve(); 364 } 365 366 return new Promise((resolve) => { 367 image.addEventListener('load', resolve, { once: true }); 368 image.addEventListener('error', resolve, { once: true }); 369 }); 370 } 371 372 toggleFullscreen() { 373 if (!document.fullscreenElement && this.root.requestFullscreen) { 374 this.root.requestFullscreen(); 375 } else if (document.exitFullscreen) { 376 document.exitFullscreen(); 377 } 378 } 379 380 dispatch(name, detail = {}) { 381 this.root.dispatchEvent(new CustomEvent(`knowolBeforeAfter:${name}`, { 382 bubbles: true, 383 detail: Object.assign({ instance: this }, detail) 384 })); 385 } 386 } 387 388 function init(root = document) { 389 root.querySelectorAll(selector).forEach((node) => { 390 if (!node.__knowolBeforeAfter) { 391 new KnowolBeforeAfter(node); 392 } 393 }); 394 } 395 396 if (document.readyState === 'loading') { 397 document.addEventListener('DOMContentLoaded', () => init()); 398 } else { 399 init(); 400 } 401 402 window.KnowolBeforeAfter = { 403 init, 404 get: (node) => node && node.__knowolBeforeAfter 405 }; 406}());
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.