PageSourceSearch

https://www.knowol.com/wp-content/plugins/knowol-image-compare/assets/js/knowol-before-after.js?ver=1.0.6

js knowol.com collected 2026-10-02 08:15:14 UTC 10,929 bytes, 406 lines download raw bytes

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.