@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.
- package/dist/ElasticCarousel-6SPFdi5u.js +1032 -0
- package/dist/ErrorMessage-WxAD9Li7.js +110 -0
- package/dist/GalleryWall-Cn7UUK_i.js +355 -0
- package/dist/Grid--7TepH_z.js +339 -0
- package/dist/PopoverGallery-oswhxqGK.js +1905 -0
- package/dist/base-D7ZLOWQ5.js +1242 -0
- package/dist/caret-right-S2XSTDFy.js +5 -0
- package/dist/demo/favicon.svg +11 -0
- package/dist/demo/index.html +205 -0
- package/dist/elements/BaseElement.d.ts +1 -1
- package/dist/elements/BaseElement.d.ts.map +1 -1
- package/dist/elements/BasePost.d.ts +1 -1
- package/dist/index-zGq7k0wG.js +938 -0
- package/dist/index.js +1 -1
- package/dist/lib/createElement.d.ts.map +1 -1
- package/dist/lib/gyre.d.ts +2 -2
- package/dist/lib/gyre.d.ts.map +1 -1
- package/dist/replaceChildren-WvILsw1L.js +32 -0
- package/dist/resizeObserver-OlrW1x9X.js +454 -0
- package/dist/widgets/GalleryWall.d.ts.map +1 -1
- package/dist/widgets/Grid.d.ts.map +1 -1
- package/package.json +2 -4
- package/dist/ElasticCarousel-TgYctl0q.js +0 -1
- package/dist/ErrorMessage-JdTd1HQ0.js +0 -1
- package/dist/GalleryWall-YhaOqfTo.js +0 -1
- package/dist/Grid-D1wqy3hY.js +0 -1
- package/dist/PopoverGallery-frjpY9Sq.js +0 -1
- package/dist/base-dR64s75u.js +0 -1
- package/dist/caret-right-xsqAZl9R.js +0 -1
- package/dist/index-P5DzRXeC.js +0 -1
- package/dist/replaceChildren-4H1qCpyP.js +0 -1
- package/dist/resizeObserver--rsjm-GV.js +0 -1
@@ -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 };
|