@behold/widget 0.5.63 → 0.5.65

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