@behold/widget 0.5.55 → 0.5.57

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,985 @@
1
+ if ('replaceChildren' in Element.prototype === false) {
2
+ function replaceChildren(...nodes) {
3
+ // Remove all existing child nodes
4
+ while (this.firstChild) {
5
+ this.removeChild(this.firstChild);
6
+ }
7
+ // Append new DOM objects
8
+ this.append(...nodes);
9
+ }
10
+ Object.defineProperty(Element.prototype, 'beholdReplaceChildren', {
11
+ configurable: true,
12
+ writable: true,
13
+ value: replaceChildren,
14
+ });
15
+ Object.defineProperty(DocumentFragment.prototype, 'beholdReplaceChildren', {
16
+ configurable: true,
17
+ writable: true,
18
+ value: replaceChildren,
19
+ });
20
+ }
21
+ else {
22
+ Object.defineProperty(Element.prototype, 'beholdReplaceChildren', {
23
+ configurable: true,
24
+ writable: true,
25
+ value: Element.prototype.replaceChildren,
26
+ });
27
+ Object.defineProperty(DocumentFragment.prototype, 'beholdReplaceChildren', {
28
+ configurable: true,
29
+ writable: true,
30
+ value: DocumentFragment.prototype.replaceChildren,
31
+ });
32
+ }
33
+
34
+ /**
35
+ * @param input Object or array to clone
36
+ * @description A simple clone using JSON.stringify and JSON.parse. Properties with an undefined value will be included in the result with a value of null
37
+ */
38
+ function clone(input) {
39
+ return JSON.parse(JSON.stringify(input, (key, value) => {
40
+ if (typeof value === 'undefined')
41
+ return null;
42
+ return value;
43
+ }));
44
+ }
45
+ /**
46
+ * @description
47
+ * Force DOM layout. Coupled with a requestAnimationFrame this can be
48
+ * used to make sure newly added elements exist in the DOM before performing
49
+ * some other action, such as an animated transition
50
+ */
51
+ function forceLayout() {
52
+ return document.body.offsetTop;
53
+ }
54
+ /**
55
+ * @async
56
+ * @param type - Media type
57
+ * @param src - Media source
58
+ */
59
+ async function preloadMedia(type, src) {
60
+ const mediaEl = document.createElement(type);
61
+ return new Promise((resolve, reject) => {
62
+ switch (type) {
63
+ case 'img':
64
+ mediaEl.addEventListener('load', () => {
65
+ resolve(src);
66
+ });
67
+ break;
68
+ case 'video':
69
+ mediaEl.addEventListener('loadeddata', () => resolve(src));
70
+ break;
71
+ }
72
+ mediaEl.addEventListener('error', (error) => {
73
+ reject(error);
74
+ });
75
+ mediaEl.src = src;
76
+ });
77
+ }
78
+ /**
79
+ * @param text - string to truncate
80
+ * @param maxLines - Restrict to n lines. Determined by line breaks
81
+ * @param maxChars - Restrict to n total characters
82
+ */
83
+ function getTruncatedText({ text, maxLines = 2, maxChars = 50 }) {
84
+ if (!text)
85
+ return;
86
+ const lines = text
87
+ .match(/.*/g)
88
+ .filter((line) => line.length > 0)
89
+ .slice(0, maxLines);
90
+ const totalLineLimitedChars = lines.join('').length;
91
+ const totalChars = Math.min(totalLineLimitedChars, maxChars);
92
+ return text.slice(0, totalChars);
93
+ }
94
+ /**
95
+ * Generate a placeholder svg image
96
+ */
97
+ function getPlaceholderImage(width, height) {
98
+ return `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' width='${width}' height='${height}'%3E%3C/svg%3E%0A`;
99
+ }
100
+ /**
101
+ * @async
102
+ * @param els - An array of elements
103
+ * @returns A promise that resolve with the index of the most visible el
104
+ * @description
105
+ * Find the most visible element in an array. Compares intersectionRatios.
106
+ * If multiple elements tie for highest intersectionRatio, the most visible
107
+ * element that appears earliest in the array is returned.
108
+ */
109
+ function getMostVisible(els) {
110
+ return new Promise((resolve) => {
111
+ let observer = new IntersectionObserver((entries) => {
112
+ let mostVisible = entries.reduce((acc, curr, index) => {
113
+ return curr.intersectionRatio > acc.intersectionRatio
114
+ ? { index, intersectionRatio: curr.intersectionRatio }
115
+ : acc;
116
+ }, { intersectionRatio: 0, index: null });
117
+ observer.disconnect();
118
+ observer = null;
119
+ resolve(mostVisible.index);
120
+ });
121
+ els.forEach((el) => observer.observe(el));
122
+ });
123
+ }
124
+ /**
125
+ * @param el - The target element
126
+ * @returns a {result: Promise, abort: Function}
127
+ * @description More performant async replacement for getBoundingClientRect
128
+ */
129
+ function getAsyncRect(el, cb) {
130
+ let aborted = false;
131
+ let observer = new IntersectionObserver((entries) => {
132
+ if (aborted)
133
+ return;
134
+ observer.disconnect();
135
+ observer = null;
136
+ cb(entries[0].boundingClientRect);
137
+ });
138
+ function abort() {
139
+ if (aborted)
140
+ return;
141
+ aborted = true;
142
+ if (observer) {
143
+ observer.disconnect();
144
+ observer = null;
145
+ }
146
+ }
147
+ observer.observe(el);
148
+ return abort;
149
+ }
150
+ /**
151
+ * @param el - the element to query
152
+ * @description
153
+ * Get first document or shadowRoot ancestor of an element
154
+ */
155
+ function getClosestShadowRootOrDocument(el) {
156
+ if (!el.isConnected)
157
+ return document;
158
+ if (el.nodeName === '#document') {
159
+ return el;
160
+ }
161
+ if (el instanceof ShadowRoot) {
162
+ return el;
163
+ }
164
+ return getClosestShadowRootOrDocument(el.parentNode);
165
+ }
166
+ /**
167
+ * @param callback - function to throttle
168
+ * @param wait - minimum time between invocations
169
+ * @param thisArg - this context to apply to callback
170
+ * @param throttleFirst - start with a delay?
171
+ */
172
+ function throttle(callback, wait, thisArg = null, throttleFirst = false) {
173
+ let lastInvocationTime = throttleFirst ? performance.now() : 0;
174
+ let finalTimeout = null;
175
+ return (...args) => {
176
+ let currentTime = performance.now();
177
+ clearTimeout(finalTimeout);
178
+ finalTimeout = setTimeout(() => callback.apply(thisArg, args), wait);
179
+ if (currentTime - lastInvocationTime > wait) {
180
+ clearTimeout(finalTimeout);
181
+ lastInvocationTime = currentTime;
182
+ callback.apply(thisArg, args);
183
+ }
184
+ };
185
+ }
186
+ /**
187
+ * @param target - An element to toggle classes on
188
+ * @param classes - An object with the format { className: boolean }
189
+ * @description
190
+ * Add or remove classes on a target element with classname keys and boolean values
191
+ */
192
+ function setClasses(target, classes) {
193
+ Object.entries(classes).forEach(([className, shouldAdd]) => {
194
+ if (shouldAdd) {
195
+ target.classList.add(className);
196
+ }
197
+ else {
198
+ target.classList.remove(className);
199
+ }
200
+ });
201
+ }
202
+ /**
203
+ * @param target - An element to set a CSS custom property on
204
+ * @param vars - An object of the form { "--var-name": "var-value" }
205
+ */
206
+ function setCssVars(target, vars) {
207
+ Object.keys(vars).forEach((key) => {
208
+ target.style.setProperty(key, vars[key]);
209
+ });
210
+ }
211
+ /**
212
+ * @param obj1 - Object to compare
213
+ * @param obj2 - Object to compare
214
+ * @param props - An array of properties to compare
215
+ * @description
216
+ * Compares the properties of two objects. Returns true if, for
217
+ * any property in the props array, obj1[prop] !== obj2[prop]
218
+ */
219
+ function hasChanges(obj1, obj2, props, depth = 0) {
220
+ if (typeof props === 'string') {
221
+ props = [props];
222
+ }
223
+ if (depth > 0) {
224
+ let val = false;
225
+ if (Object.keys(obj1).length !== Object.keys(obj2).length) {
226
+ return true;
227
+ }
228
+ Object.keys(obj1).forEach((key) => {
229
+ if (hasChanges(obj1[key], obj2[key], props)) {
230
+ val = true;
231
+ }
232
+ });
233
+ return val;
234
+ }
235
+ return props.reduce((acc, curr) => {
236
+ if (!isEqual(obj1?.[curr], obj2?.[curr]))
237
+ return true;
238
+ return acc;
239
+ }, false);
240
+ }
241
+ /**
242
+ * @param a - value to compare
243
+ * @param b - value to compare
244
+ * @param fuzzyNumbers - if set to true '1' will be considered equal to 1. WARNING: this mutates inputs
245
+ * @param sortArrays - if set to true arrays are sorted before comparing. WARNING: this mutates inputs
246
+ * @description
247
+ * Check if two values are the same
248
+ */
249
+ function isEqual(a, b, fuzzyNumbers = false, sortArrays = false) {
250
+ if (sortArrays) {
251
+ if (Array.isArray(a))
252
+ a = a.sort();
253
+ if (Array.isArray(b))
254
+ b = b.sort();
255
+ }
256
+ if (fuzzyNumbers) {
257
+ if (typeof a === 'number')
258
+ a = a.toString();
259
+ if (typeof b === 'number')
260
+ b = b.toString();
261
+ }
262
+ if (typeof a !== typeof b)
263
+ return false;
264
+ if (a === null && b === null)
265
+ return true;
266
+ if (a === null && b !== null)
267
+ return false;
268
+ if (a !== null && b === null)
269
+ return false;
270
+ if (a === undefined && b === undefined)
271
+ return true;
272
+ if (a === undefined && b !== undefined)
273
+ return false;
274
+ if (a !== undefined && b === undefined)
275
+ return false;
276
+ if (typeof a === 'object') {
277
+ if (Object.keys(a).length !== Object.keys(b).length)
278
+ return false;
279
+ return Object.entries(a).reduce((acc, [key, val]) => {
280
+ return isEqual(val, b[key], fuzzyNumbers, sortArrays) ? acc : false;
281
+ }, true);
282
+ }
283
+ else if (Array.isArray(a)) {
284
+ let arrayIsEqual = true;
285
+ a.forEach((arrVal, index) => {
286
+ if (!isEqual(arrVal, b[index], fuzzyNumbers, sortArrays))
287
+ arrayIsEqual = false;
288
+ });
289
+ return arrayIsEqual;
290
+ }
291
+ else {
292
+ return a === b;
293
+ }
294
+ }
295
+ /**
296
+ * @param text - The text to announce
297
+ * @param mood - assertive or polite
298
+ * @description - announces some text to screenreaders only
299
+ */
300
+ function announceToScreenReader(text, mood = 'assertive') {
301
+ const containerEl = document.createElement('div');
302
+ containerEl.innerHTML = text;
303
+ containerEl.setAttribute('aria-live', mood);
304
+ containerEl.setAttribute('aria-atomic', 'true');
305
+ containerEl.style.cssText = `
306
+ position: absolute;
307
+ position: absolute !important;
308
+ width: 1px !important;
309
+ height: 1px !important;
310
+ padding: 0 !important;
311
+ margin: -1px !important;
312
+ overflow: hidden !important;
313
+ clip: rect(0,0,0,0) !important;
314
+ white-space: nowrap !important;
315
+ border: 0 !important;
316
+ `;
317
+ requestAnimationFrame(() => {
318
+ document.body.appendChild(containerEl);
319
+ setTimeout(() => {
320
+ containerEl.remove();
321
+ }, 500);
322
+ });
323
+ }
324
+ /**
325
+ * @param array - Array to push value into
326
+ * @param val - Value to push
327
+ * @param limit - Array length limit
328
+ * @description - Push a value into an array, remove from beginning of array if array.length > limit
329
+ */
330
+ function pushWithLimit(array, val, limit) {
331
+ if (array.length === limit) {
332
+ array.shift();
333
+ }
334
+ array.push(val);
335
+ }
336
+
337
+ // Global state is shared between all elements across all widgets on the page
338
+ let globalState = {
339
+ isMuted: true,
340
+ keyboardNavEnabled: false,
341
+ popoverOverlayHslArray: [0, 0, 0],
342
+ popoverOverlayOpacity: 0.6,
343
+ };
344
+ let globalListenersAdded = false;
345
+ let sharedResizeObserver = null;
346
+ let sharedIntersectionObserver = null;
347
+ const resizeHandlers = new Map();
348
+ const intersectionHandlers = new Map();
349
+ const loopHandlers = new Map();
350
+ const globalStateChangeHandlers = new Set();
351
+ function loop() {
352
+ loopHandlers.forEach((item) => {
353
+ const now = performance.now();
354
+ const { minWait, lastInvocation, callback } = item;
355
+ const timeSinceLastInvocation = now - lastInvocation;
356
+ if (timeSinceLastInvocation > minWait) {
357
+ callback(timeSinceLastInvocation);
358
+ loopHandlers.set(callback, { ...item, lastInvocation: now });
359
+ }
360
+ });
361
+ requestAnimationFrame(loop);
362
+ }
363
+ loop();
364
+ class BaseElement extends HTMLElement {
365
+ label = 'BaseElement';
366
+ storedProperties = new Map();
367
+ requiredProperties = [];
368
+ hasRequiredProps = false;
369
+ propChangeHandlers = new Map();
370
+ delayedPropChangeHandlers = new Map();
371
+ queuedPropUpdates = new Set();
372
+ localState = Object.freeze({});
373
+ localStateChangeHandlers = new Set();
374
+ connectHandlers = new Set();
375
+ disconnectHandlers = new Set();
376
+ rafs = new Map();
377
+ timeouts = new Map();
378
+ constructor() {
379
+ super();
380
+ this._globalHandleKeydown = this._globalHandleKeydown.bind(this);
381
+ this._globalHandleMousedown = this._globalHandleMousedown.bind(this);
382
+ this._globalHandleMousemove = this._globalHandleMousemove.bind(this);
383
+ if (!sharedResizeObserver) {
384
+ let ResizeObserver = window.ResizeObserver;
385
+ if ('ResizeObserver' in window === false) {
386
+ // @ts-ignore
387
+ ResizeObserver = window.BeholdResizeObserver;
388
+ }
389
+ sharedResizeObserver = new ResizeObserver((entries) => {
390
+ entries.forEach((entry) => {
391
+ const cb = resizeHandlers.get(entry.target);
392
+ if (cb) {
393
+ cb(entry);
394
+ }
395
+ });
396
+ });
397
+ }
398
+ if (!sharedIntersectionObserver) {
399
+ sharedIntersectionObserver = new IntersectionObserver((entries) => {
400
+ entries.forEach((entry) => {
401
+ const cb = intersectionHandlers.get(entry.target);
402
+ if (cb) {
403
+ cb(entry);
404
+ }
405
+ });
406
+ });
407
+ }
408
+ }
409
+ /**
410
+ * Connect
411
+ */
412
+ connectedCallback() {
413
+ if (!globalListenersAdded) {
414
+ globalListenersAdded = true;
415
+ document.addEventListener('keydown', this._globalHandleKeydown);
416
+ document.addEventListener('mousedown', this._globalHandleMousedown);
417
+ document.addEventListener('mousemove', this._globalHandleMousemove, {
418
+ passive: true,
419
+ });
420
+ }
421
+ this.connectHandlers.forEach((cb) => cb());
422
+ }
423
+ /*
424
+ * Clean up
425
+ */
426
+ disconnectedCallback() {
427
+ if (globalListenersAdded) {
428
+ globalListenersAdded = false;
429
+ document.removeEventListener('keydown', this._globalHandleKeydown);
430
+ document.removeEventListener('mousedown', this._globalHandleMousedown);
431
+ document.removeEventListener('mousemove', this._globalHandleMousemove);
432
+ }
433
+ this.rafs.forEach((rafId) => cancelAnimationFrame(rafId));
434
+ this.rafs.clear();
435
+ this.timeouts.forEach((toId) => clearTimeout(toId));
436
+ this.timeouts.clear();
437
+ this.disconnectHandlers.forEach((cb) => cb());
438
+ }
439
+ /**
440
+ * Register a callback to fire on connect
441
+ */
442
+ onConnect(cb) {
443
+ this.connectHandlers.add(cb);
444
+ }
445
+ /**
446
+ * Register a callback to fire on disconnect
447
+ */
448
+ onDisconnect(cb) {
449
+ this.disconnectHandlers.add(cb);
450
+ }
451
+ /**
452
+ * Register a callback to fire on resize
453
+ */
454
+ onResize(context, el, cb) {
455
+ resizeHandlers.set(el, cb.bind(context));
456
+ sharedResizeObserver.observe(el);
457
+ this.disconnectHandlers.add(() => {
458
+ resizeHandlers.delete(el);
459
+ sharedResizeObserver.unobserve(el);
460
+ });
461
+ }
462
+ /**
463
+ * Register a callback to fire on intersection
464
+ *
465
+ * @param el - element to observe
466
+ */
467
+ onIntersection(el, cb) {
468
+ intersectionHandlers.set(el, cb);
469
+ sharedIntersectionObserver.observe(el);
470
+ this.disconnectHandlers.add(() => {
471
+ intersectionHandlers.delete(el);
472
+ sharedIntersectionObserver.unobserve(el);
473
+ });
474
+ }
475
+ /**
476
+ * Register a callback to fire on intersection
477
+ *
478
+ * @param callback - callback
479
+ * @param minWait - min time between invocations
480
+ */
481
+ onLoop(callback, minWait = 50) {
482
+ loopHandlers.set(callback, { minWait, lastInvocation: 0, callback });
483
+ this.disconnectHandlers.add(() => {
484
+ loopHandlers.delete(callback);
485
+ });
486
+ }
487
+ /**
488
+ *
489
+ */
490
+ /**
491
+ * @param handler - Will be called whenever a prop changes with a single object argument: {changedProp, oldValue, newValue}
492
+ * @param props - An array of prop names to subscribe to
493
+ * @param required - An array of props that must be set before any handlers are fired. All listed props are required by default or if set to null. No props are required if passed an empty array
494
+ * @param setupFunction - A function to run after all required props are set. If this is defined, individual prop change handlers won't be fired during initial setup
495
+ * @description
496
+ * Register a callback to fire when a property changes. Undefined props in objects will be coerced to null
497
+ */
498
+ onPropChange(handler, props, required = null, setupFunction = null) {
499
+ if (required) {
500
+ this.requiredProperties.push(...required);
501
+ }
502
+ else {
503
+ this.requiredProperties.push(...props);
504
+ }
505
+ props.forEach((prop) => {
506
+ // Store prop value
507
+ if (typeof this[prop] !== 'undefined') {
508
+ this.storedProperties.set(prop, this[prop]);
509
+ }
510
+ // Create an empty array of handlers and delayed handlers if they don't exist yet
511
+ if (!this.propChangeHandlers.get(prop)) {
512
+ this.propChangeHandlers.set(prop, []);
513
+ this.delayedPropChangeHandlers.set(prop, []);
514
+ }
515
+ // Add handler to prop
516
+ if (setupFunction) {
517
+ // There is a setup function, so we wait until after setup to start handling this prop
518
+ this.delayedPropChangeHandlers.get(prop).push(handler);
519
+ }
520
+ else {
521
+ // No setup function, immediately start firing handlers when this prop changes
522
+ this.propChangeHandlers.get(prop).push(handler);
523
+ }
524
+ // Define Getter & Setter
525
+ Object.defineProperty(this, prop, {
526
+ set(newValue) {
527
+ // No undefined props allowed
528
+ if (newValue === undefined) {
529
+ newValue = null;
530
+ console.warn(`Attempted to set value of ${prop} as "undefined" on ${this.label}. ${prop} was coerced to null instead.`);
531
+ }
532
+ // Coerces undefined props in newValue to null
533
+ newValue = clone(newValue);
534
+ // Save previous value
535
+ const oldValue = this.storedProperties.has(prop)
536
+ ? clone(this.storedProperties.get(prop))
537
+ : null;
538
+ // Update props with new value
539
+ this.storedProperties.set(prop, newValue);
540
+ // Check if all required props are present
541
+ this.hasRequiredProps = this.requiredProperties.every((prop) => this.storedProperties.has(prop));
542
+ // Queue handlers
543
+ this.queueHandlers({ prop, oldValue, newValue });
544
+ },
545
+ get() {
546
+ return this.storedProperties.get(prop);
547
+ },
548
+ });
549
+ });
550
+ // All required props are defined
551
+ if (setupFunction) {
552
+ setupFunction = setupFunction.bind(this);
553
+ queueMicrotask(() => {
554
+ if (this.hasRequiredProps) {
555
+ // Run setup function
556
+ setupFunction();
557
+ // Add delayed handlers to handlers map
558
+ this.delayedPropChangeHandlers.forEach((handlers, prop) => {
559
+ this.propChangeHandlers.set(prop, [
560
+ ...this.propChangeHandlers.get(prop),
561
+ ...handlers,
562
+ ]);
563
+ });
564
+ }
565
+ });
566
+ }
567
+ // Log a warning if there are missing required props
568
+ this.queueMissingPropsCheck();
569
+ }
570
+ /*
571
+ * Queue prop change handlers
572
+ */
573
+ queueHandlers({ prop, oldValue, newValue }) {
574
+ // We're missing required props. Add handler to queue
575
+ if (!this.hasRequiredProps) {
576
+ this.queuedPropUpdates.add(prop);
577
+ // All required props are present
578
+ }
579
+ else {
580
+ // Run queued prop handlers
581
+ if (this.queuedPropUpdates.size) {
582
+ // Remove current prop from queue. It will get handled in the next step
583
+ this.queuedPropUpdates.delete(prop);
584
+ // Run queued handlers
585
+ this.runQueuedPropHandlers();
586
+ }
587
+ // Run handlers for current prop
588
+ this.propChangeHandlers.get(prop).forEach((cb) => {
589
+ cb.call(this, {
590
+ changedProp: prop,
591
+ oldValue,
592
+ newValue: newValue,
593
+ });
594
+ });
595
+ }
596
+ }
597
+ /*
598
+ * Run queued handlers
599
+ * @param context - Handlers will be called with this context as the 'this' value
600
+ */
601
+ runQueuedPropHandlers() {
602
+ this.queuedPropUpdates.forEach((prop) => {
603
+ this.propChangeHandlers.get(prop).forEach((cb) => {
604
+ cb.call(this, {
605
+ changedProp: prop,
606
+ oldValue: null,
607
+ newValue: this[prop],
608
+ });
609
+ });
610
+ });
611
+ this.queuedPropUpdates.clear();
612
+ }
613
+ /**
614
+ * @param val - Object to be merged with current shared state
615
+ * @description - Update shared state
616
+ */
617
+ updateLocalState(updates) {
618
+ const oldState = Object.freeze(clone(this.localState));
619
+ this.localState = Object.freeze({ ...this.localState, ...updates });
620
+ this.localStateChangeHandlers.forEach((handler) => handler({
621
+ changedProps: Object.keys(updates),
622
+ oldState,
623
+ newState: this.localState,
624
+ }));
625
+ }
626
+ /**
627
+ * @param handler - Will be passed an object when local state is updated: {changedProps, oldState, newState}
628
+ * @description - Register a callback to fire on shared state change
629
+ */
630
+ onLocalStateChange(handler, defaultValue) {
631
+ this.localState = { ...this.localState, ...defaultValue };
632
+ const func = handler.bind(this);
633
+ this.localStateChangeHandlers.add(func);
634
+ }
635
+ /**
636
+ * @param val - Object to be merged with current shared state
637
+ * @description - Update shared state
638
+ */
639
+ updateGlobalState(val) {
640
+ const oldState = clone(globalState);
641
+ globalState = { ...globalState, ...val };
642
+ globalStateChangeHandlers.forEach((handler) => handler({
643
+ changedProps: Object.keys(val),
644
+ oldState,
645
+ newState: globalState,
646
+ }));
647
+ }
648
+ /**
649
+ * @param handler - Will be passed an object when global state is updated: {changedProps, oldState, newState}
650
+ * @description - Register a callback to fire on shared state change
651
+ */
652
+ onGlobalStateChange(handler) {
653
+ this.connectHandlers.add(() => {
654
+ const func = handler.bind(this);
655
+ const removeFunc = () => globalStateChangeHandlers.delete(func);
656
+ globalStateChangeHandlers.add(func);
657
+ this.disconnectHandlers.add(removeFunc);
658
+ });
659
+ }
660
+ /**
661
+ * @description global keydown handler
662
+ */
663
+ _globalHandleKeydown(evt) {
664
+ if (evt.key === 'Tab') {
665
+ this.updateGlobalState({ keyboardNavEnabled: true });
666
+ }
667
+ }
668
+ /**
669
+ * @description global mousedown handler
670
+ */
671
+ _globalHandleMousedown(evt) {
672
+ if (evt.clientX === 0 && evt.clientY === 0)
673
+ return;
674
+ this.updateGlobalState({ keyboardNavEnabled: false });
675
+ }
676
+ /**
677
+ * @description global mousemove handler
678
+ */
679
+ _globalHandleMousemove(evt) {
680
+ if (evt.ctrlKey ||
681
+ evt.altKey ||
682
+ evt.shiftKey ||
683
+ evt.metaKey ||
684
+ evt.movementX === 0 ||
685
+ evt.movementY === 0) {
686
+ return;
687
+ }
688
+ this.updateGlobalState({ keyboardNavEnabled: false });
689
+ }
690
+ /**
691
+ * @description
692
+ * Get global state value. This state object is shared between all elements across all widget instances on the page
693
+ */
694
+ get globalState() {
695
+ return globalState;
696
+ }
697
+ /**
698
+ * @description
699
+ * Shared state cannot be set directly. Use this.updateGlobalState() instead.
700
+ */
701
+ set globalState(val) {
702
+ throw new Error('Shared state cannot be set directly. Use this.updateGlobalState() instead.');
703
+ }
704
+ /**
705
+ * @param cb - callback function
706
+ * @param id - An id for this raf. Can be used to cancel it with cancelRaf()
707
+ * @description - requestAnimationFrame with cleanup
708
+ */
709
+ raf(cb, id, cancelPrev = true) {
710
+ if (cancelPrev) {
711
+ cancelAnimationFrame(this.rafs.get(id));
712
+ }
713
+ const rafId = requestAnimationFrame(() => {
714
+ cb();
715
+ this.rafs.delete(id);
716
+ });
717
+ this.rafs.set(id, rafId);
718
+ return rafId;
719
+ }
720
+ /**
721
+ * @param id - id of the raf, set by previously called raf()
722
+ * @description - Cancel a raf by id
723
+ */
724
+ cancelRaf(id) {
725
+ cancelAnimationFrame(this.rafs.get(id));
726
+ this.rafs.delete(id);
727
+ }
728
+ /**
729
+ * @param cb - callback function
730
+ * @param delay - timeout delay
731
+ * @param id - An id for this raf. Can be used to cancel it with cancelTo()
732
+ * @description - setTimeout with cleanup
733
+ */
734
+ to(cb, delay, id) {
735
+ const toId = setTimeout(() => {
736
+ cb();
737
+ this.timeouts.delete(id);
738
+ }, delay);
739
+ this.timeouts.set(id, toId);
740
+ return toId;
741
+ }
742
+ /**
743
+ * @param id - id of the timeout, set by previously called to()
744
+ * @description - Cancel a timeout by id
745
+ */
746
+ cancelTo(id) {
747
+ clearTimeout(this.timeouts.get(id));
748
+ this.timeouts.delete(id);
749
+ }
750
+ /**
751
+ * @description
752
+ * Log a warning if any required props aren't set in the same execution context that the element is created
753
+ */
754
+ queueMissingPropsCheck() {
755
+ queueMicrotask(() => {
756
+ if (this.requiredProperties.length && !this.hasRequiredProps) {
757
+ const missingProps = this.requiredProperties.filter((prop) => !this.storedProperties.has(prop));
758
+ console.error(`${this.label || this.tagName} is missing required props: ${missingProps.join(', ')}`);
759
+ }
760
+ });
761
+ }
762
+ }
763
+
764
+ /*
765
+ * Create an El
766
+ */
767
+ function createElement(args) {
768
+ let { type = 'div', classes = [], contents = [], attributes = {}, props = {}, style = {}, listeners = {}, } = args;
769
+ classes = Array.isArray(classes) ? classes : [classes];
770
+ const el = document.createElement(type);
771
+ if (!Array.isArray(contents))
772
+ contents = [contents];
773
+ // Render SVG strings
774
+ contents = contents
775
+ .filter((item) => typeof item !== 'undefined' && item !== null)
776
+ .map((item) => {
777
+ if (typeof item === 'string' && item.includes('</svg>')) {
778
+ const temp = document.createElement('template');
779
+ temp.innerHTML = item;
780
+ return temp.content;
781
+ }
782
+ return item;
783
+ });
784
+ el.beholdReplaceChildren(...contents);
785
+ if (classes.length) {
786
+ el.className = classes.join(' ');
787
+ }
788
+ Object.keys(attributes).forEach((key) => {
789
+ if (attributes[key] !== null && typeof attributes[key] !== 'undefined') {
790
+ el.setAttribute(key, attributes[key]);
791
+ }
792
+ else {
793
+ el.removeAttribute(key);
794
+ }
795
+ });
796
+ Object.assign(el, props);
797
+ Object.keys(style).forEach((key) => {
798
+ if (key.substring(0, 2) === '--') {
799
+ el.style.setProperty(key, style[key]);
800
+ }
801
+ else {
802
+ el.style[key] = style[key];
803
+ }
804
+ });
805
+ Object.keys(listeners).forEach((key) => {
806
+ el.addEventListener(key, listeners[key]);
807
+ });
808
+ return el;
809
+ }
810
+
811
+ function debugLog(...args) {
812
+ // @ts-ignore
813
+ if (window?.location.search.includes('behold-debug-mode')) {
814
+ console.log(...args);
815
+ }
816
+ }
817
+
818
+ var css_248z = ":host{align-items:center;box-sizing:border-box;display:flex;flex-wrap:wrap;justify-content:center;margin:0;min-width:50px;overflow:hidden;position:relative;width:100%}:host *{box-sizing:border-box}:host([hidden]){display:none}";
819
+
820
+ function __variableDynamicImportRuntime1__(path) {
821
+ switch (path) {
822
+ case './widgets/ElasticCarousel.ts': return import('./ElasticCarousel-WWMTzi-V.js');
823
+ case './widgets/ErrorMessage.ts': return import('./ErrorMessage-tHLrPf_h.js');
824
+ case './widgets/GalleryWall.ts': return import('./GalleryWall-Jlau7S7U.js');
825
+ case './widgets/Grid.ts': return import('./Grid-2Aag90e0.js');
826
+ default: return new Promise(function(resolve, reject) {
827
+ (typeof queueMicrotask === 'function' ? queueMicrotask : setTimeout)(
828
+ reject.bind(null, new Error("Unknown variable dynamic import: " + path))
829
+ );
830
+ })
831
+ }
832
+ }
833
+ /*
834
+ * BeholdWidget
835
+ * Accepts a feed ID and dynamically loads the correct widget type
836
+ */
837
+ class BeholdWidget extends BaseElement {
838
+ label = 'BeholdWidget';
839
+ shadow;
840
+ abortController;
841
+ loadedWidget;
842
+ static get observedAttributes() {
843
+ return ['feed-id'];
844
+ }
845
+ constructor() {
846
+ super();
847
+ debugLog('v 0.5.57');
848
+ this.shadow = this.attachShadow({ mode: 'open' });
849
+ this.abortController = null;
850
+ this.onPropChange(this._handlePropChange, [
851
+ 'widgetSettings',
852
+ 'feedMetadata',
853
+ 'posts',
854
+ 'previewLoadingColors',
855
+ 'errorMessage',
856
+ ], []);
857
+ }
858
+ /**
859
+ * Attributes change
860
+ */
861
+ attributeChangedCallback(changed, oldValue, newValue) {
862
+ if (changed === 'feed-id' && newValue !== oldValue) {
863
+ this.getFeed(newValue);
864
+ }
865
+ }
866
+ /**
867
+ * Prop change
868
+ */
869
+ _handlePropChange({ changedProp, oldValue, newValue }) {
870
+ if (changedProp === 'errorMessage' && !oldValue && !!newValue) {
871
+ this.loadWidget('ErrorMessage');
872
+ return;
873
+ }
874
+ if (changedProp === 'widgetSettings' &&
875
+ hasChanges(newValue, oldValue, 'type') &&
876
+ newValue?.type) {
877
+ this.loadWidget(newValue.type);
878
+ return;
879
+ }
880
+ if (this.loadedWidget && !isEqual(newValue, oldValue)) {
881
+ this.loadedWidget[changedProp] = newValue;
882
+ }
883
+ }
884
+ /*
885
+ * Load a widget
886
+ */
887
+ async loadWidget(type) {
888
+ if ('ResizeObserver' in window === false) {
889
+ const { ResizeObserver } = await import('./resizeObserver-OlrW1x9X.js');
890
+ // @ts-ignore
891
+ window.BeholdResizeObserver = ResizeObserver;
892
+ }
893
+ const Widget = await this.importWidget(type.replace(/.?/, (match) => {
894
+ return match.toUpperCase();
895
+ }));
896
+ const elName = Widget.register();
897
+ await customElements.whenDefined(elName);
898
+ const contents = createElement({ type: elName });
899
+ const styleEl = createElement({ type: 'style', contents: css_248z.toString() });
900
+ this.shadow.beholdReplaceChildren(contents, styleEl);
901
+ this.loadedWidget = contents;
902
+ if (this.widgetSettings) {
903
+ this.loadedWidget.widgetSettings = this.widgetSettings;
904
+ }
905
+ if (this.feedMetadata) {
906
+ this.loadedWidget.feedMetadata = this.feedMetadata;
907
+ }
908
+ if (this.posts) {
909
+ this.loadedWidget.posts = this.posts;
910
+ }
911
+ this.loadedWidget.errorMessage = this.errorMessage || null;
912
+ this.dispatchEvent(new Event('load'));
913
+ // Included for backwards compatibility
914
+ window.dispatchEvent(new CustomEvent('behold:widget-loaded', {
915
+ detail: { id: this['feed-id'] },
916
+ }));
917
+ }
918
+ /*
919
+ * Fetch a feed
920
+ */
921
+ async getFeed(feedId) {
922
+ if (!feedId) {
923
+ this.errorMessage = 'No feed ID provided';
924
+ return;
925
+ }
926
+ if (this.abortController) {
927
+ this.abortController.abort();
928
+ }
929
+ this.abortController = new AbortController();
930
+ try {
931
+ const rawRes = await fetch(`https://feeds.behold.so/${feedId}`, {
932
+ mode: 'cors',
933
+ signal: this.abortController.signal,
934
+ });
935
+ if (rawRes.ok) {
936
+ const feedJson = await rawRes.json();
937
+ if (!feedJson.widgetSettings) {
938
+ throw new Error('JSON feeds cannot be used as a widget');
939
+ }
940
+ if (feedJson.widgetSettings.klaviyoWebFeedId) {
941
+ throw new Error('Klaviyo feeds cannot be used as an embedded widget');
942
+ }
943
+ this.widgetSettings = feedJson.widgetSettings;
944
+ this.feedMetadata = {
945
+ username: feedJson.username,
946
+ profilePictureUrl: feedJson.profilePictureUrl,
947
+ website: feedJson.website,
948
+ followersCount: feedJson.followersCount,
949
+ hashtags: feedJson.hashtags,
950
+ };
951
+ this.posts = feedJson.posts || feedJson.media; // Support v1 && v2 feeds
952
+ }
953
+ else {
954
+ const errorMessage = await rawRes.text();
955
+ this.errorMessage = errorMessage;
956
+ }
957
+ }
958
+ catch (error) {
959
+ if (error.message !== 'The user aborted a request.') {
960
+ this.errorMessage = error.message;
961
+ }
962
+ }
963
+ }
964
+ /*
965
+ * Dynamically import a widget
966
+ */
967
+ async importWidget(widgetName) {
968
+ const { default: widget } = await __variableDynamicImportRuntime1__(`./widgets/${widgetName}.ts`);
969
+ return widget;
970
+ }
971
+ }
972
+ /**
973
+ * Export register function
974
+ */
975
+ var Widget = {
976
+ register: (name = 'behold-widget') => {
977
+ if (!customElements.get(name)) {
978
+ customElements.define(name, class extends BeholdWidget {
979
+ });
980
+ }
981
+ },
982
+ element: BeholdWidget,
983
+ };
984
+
985
+ export { BaseElement as B, Widget as W, setClasses as a, getPlaceholderImage as b, createElement as c, preloadMedia as d, getClosestShadowRootOrDocument as e, forceLayout as f, getAsyncRect as g, hasChanges as h, isEqual as i, getMostVisible as j, announceToScreenReader as k, getTruncatedText as l, pushWithLimit as p, setCssVars as s, throttle as t };