@symbiotejs/symbiote 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1394 @@
1
+ /** @returns {Object<string, any>} */
2
+ function cloneObj(obj) {
3
+ let clone = (o) => {
4
+ for (let prop in o) {
5
+ if (o[prop]?.constructor === Object) {
6
+ o[prop] = clone(o[prop]);
7
+ }
8
+ }
9
+ return { ...o };
10
+ };
11
+ return clone(obj);
12
+ }
13
+
14
+ class Data {
15
+ /**
16
+ * @param {Object} src
17
+ * @param {String} [src.name]
18
+ * @param {Object<string, any>} src.schema
19
+ */
20
+ constructor(src) {
21
+ this.uid = Symbol();
22
+ this.name = src.name || null;
23
+ if (src.schema.constructor === Object) {
24
+ this.store = cloneObj(src.schema);
25
+ } else {
26
+ // For Proxy support:
27
+ this._storeIsProxy = true;
28
+ this.store = src.schema;
29
+ }
30
+ /** @type {Object<String, Set<Function>>} */
31
+ this.callbackMap = Object.create(null);
32
+ }
33
+
34
+ /**
35
+ * @param {String} actionName
36
+ * @param {String} prop
37
+ */
38
+ static warn(actionName, prop) {
39
+ console.warn(`Symbiote Data: cannot ${actionName}. Prop name: ` + prop);
40
+ }
41
+
42
+ /** @param {String} prop */
43
+ read(prop) {
44
+ if (!this._storeIsProxy && !this.store.hasOwnProperty(prop)) {
45
+ Data.warn('read', prop);
46
+ return null;
47
+ }
48
+ return this.store[prop];
49
+ }
50
+
51
+ /** @param {String} prop */
52
+ has(prop) {
53
+ return this._storeIsProxy ? this.store[prop] !== undefined : this.store.hasOwnProperty(prop);
54
+ }
55
+
56
+ /**
57
+ * @param {String} prop
58
+ * @param {any} val
59
+ * @param {Boolean} [rewrite]
60
+ */
61
+ add(prop, val, rewrite = true) {
62
+ if (!rewrite && Object.keys(this.store).includes(prop)) {
63
+ return;
64
+ }
65
+ this.store[prop] = val;
66
+ if (this.callbackMap[prop]) {
67
+ this.callbackMap[prop].forEach((callback) => {
68
+ callback(this.store[prop]);
69
+ });
70
+ }
71
+ }
72
+
73
+ /**
74
+ * @param {String} prop
75
+ * @param {any} val
76
+ */
77
+ pub(prop, val) {
78
+ if (!this._storeIsProxy && !this.store.hasOwnProperty(prop)) {
79
+ Data.warn('publish', prop);
80
+ return;
81
+ }
82
+ this.add(prop, val);
83
+ }
84
+
85
+ /** @param {Object<string, any>} updObj */
86
+ multiPub(updObj) {
87
+ for (let prop in updObj) {
88
+ this.pub(prop, updObj[prop]);
89
+ }
90
+ }
91
+
92
+ /** @param {String} prop */
93
+ notify(prop) {
94
+ if (this.callbackMap[prop]) {
95
+ this.callbackMap[prop].forEach((callback) => {
96
+ callback(this.store[prop]);
97
+ });
98
+ }
99
+ }
100
+
101
+ /**
102
+ * @param {String} prop
103
+ * @param {Function} callback
104
+ * @param {Boolean} [init]
105
+ */
106
+ sub(prop, callback, init = true) {
107
+ if (!this._storeIsProxy && !this.store.hasOwnProperty(prop)) {
108
+ Data.warn('subscribe', prop);
109
+ return null;
110
+ }
111
+ if (!this.callbackMap[prop]) {
112
+ this.callbackMap[prop] = new Set();
113
+ }
114
+ this.callbackMap[prop].add(callback);
115
+ if (init) {
116
+ callback(this.store[prop]);
117
+ }
118
+ return {
119
+ remove: () => {
120
+ this.callbackMap[prop].delete(callback);
121
+ if (!this.callbackMap[prop].size) {
122
+ delete this.callbackMap[prop];
123
+ }
124
+ },
125
+ callback,
126
+ };
127
+ }
128
+
129
+ remove() {
130
+ delete Data.globalStore[this.uid];
131
+ }
132
+
133
+ /** @param {Object<string, any>} schema */
134
+ static registerLocalCtx(schema) {
135
+ let state = new Data({
136
+ schema,
137
+ });
138
+ Data.globalStore[state.uid] = state;
139
+ return state;
140
+ }
141
+
142
+ /**
143
+ * @param {String} ctxName
144
+ * @param {Object<string, any>} schema
145
+ * @returns {Data}
146
+ */
147
+ static registerNamedCtx(ctxName, schema) {
148
+ /** @type {Data} */
149
+ let state = Data.globalStore[ctxName];
150
+ if (state) {
151
+ console.warn('State: context name "' + ctxName + '" already in use');
152
+ } else {
153
+ state = new Data({
154
+ name: ctxName,
155
+ schema,
156
+ });
157
+ Data.globalStore[ctxName] = state;
158
+ }
159
+ return state;
160
+ }
161
+
162
+ /** @param {String} ctxName */
163
+ static clearNamedCtx(ctxName) {
164
+ delete Data.globalStore[ctxName];
165
+ }
166
+
167
+ /**
168
+ * @param {String} ctxName
169
+ * @param {Boolean} [notify]
170
+ * @returns {Data}
171
+ */
172
+ static getNamedCtx(ctxName, notify = true) {
173
+ return Data.globalStore[ctxName] || (notify && console.warn('State: wrong context name - "' + ctxName + '"'), null);
174
+ }
175
+ }
176
+
177
+ Data.globalStore = Object.create(null);
178
+
179
+ const DICT = Object.freeze({
180
+ // Template data binding attribute:
181
+ BIND_ATTR: 'set',
182
+ // Local state binding attribute name:
183
+ ATTR_BIND_PRFX: '@',
184
+ // External prop prefix:
185
+ EXT_DATA_CTX_PRFX: '*',
186
+ // Named data context property splitter:
187
+ NAMED_DATA_CTX_SPLTR: '/',
188
+ // Data context name attribute:
189
+ CTX_NAME_ATTR: 'ctx-name',
190
+ // Data context name in CSS custom property:
191
+ CSS_CTX_PROP: '--ctx-name',
192
+ // Element reference attribute:
193
+ EL_REF_ATTR: 'ref',
194
+ // Prefix for auto generated tag names:
195
+ AUTO_TAG_PRFX: 'sym',
196
+ });
197
+
198
+ const CHARS = '1234567890QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm';
199
+ const CHLENGTH = CHARS.length - 1;
200
+
201
+ class UID {
202
+
203
+ /**
204
+ *
205
+ * @param {String} [pattern] any symbols sequence with dashes. Default dash is used for human readability
206
+ * @returns {String} output example: v6xYaSk7C-kzZ
207
+ */
208
+ static generate(pattern = 'XXXXXXXXX-XXX') {
209
+ let uid = '';
210
+ for (let i = 0; i < pattern.length; i++) {
211
+ uid += pattern[i] === '-' ? pattern[i] : CHARS.charAt(Math.random() * CHLENGTH);
212
+ }
213
+ return uid;
214
+ }
215
+ }
216
+
217
+ function slotProcessor(fr, fnCtx) {
218
+ if (fnCtx.renderShadow) {
219
+ return;
220
+ }
221
+ let slots = [...fr.querySelectorAll('slot')];
222
+ if (fnCtx.__initChildren.length && slots.length) {
223
+ let slotMap = {};
224
+ slots.forEach((slot) => {
225
+ let slotName = slot.getAttribute('name');
226
+ if (slotName) {
227
+ slotMap[slotName] = {
228
+ slot,
229
+ fr: document.createDocumentFragment(),
230
+ };
231
+ } else {
232
+ slotMap.__default__ = {
233
+ slot,
234
+ fr: document.createDocumentFragment(),
235
+ };
236
+ }
237
+ });
238
+ fnCtx.__initChildren.forEach((/** @type {Element} */ child) => {
239
+ let slotName = child.getAttribute?.('slot');
240
+ if (slotName) {
241
+ child.removeAttribute('slot');
242
+ slotMap[slotName].fr.appendChild(child);
243
+ } else if (slotMap.__default__) {
244
+ slotMap.__default__.fr.appendChild(child);
245
+ }
246
+ });
247
+ Object.values(slotMap).forEach((mapObj) => {
248
+ mapObj.slot.parentNode.insertBefore(mapObj.fr, mapObj.slot);
249
+ mapObj.slot.remove();
250
+ });
251
+ } else {
252
+ fnCtx.innerHTML = '';
253
+ }
254
+ }
255
+
256
+ function refProcessor(fr, fnCtx) {
257
+ [...fr.querySelectorAll(`[${DICT.EL_REF_ATTR}]`)].forEach((/** @type {HTMLElement} */ el) => {
258
+ let refName = el.getAttribute(DICT.EL_REF_ATTR);
259
+ fnCtx.ref[refName] = el;
260
+ el.removeAttribute(DICT.EL_REF_ATTR);
261
+ });
262
+ }
263
+
264
+ /**
265
+ * @param {DocumentFragment} fr
266
+ * @param {any} fnCtx
267
+ */
268
+ function domSetProcessor(fr, fnCtx) {
269
+ [...fr.querySelectorAll(`[${DICT.BIND_ATTR}]`)].forEach((el) => {
270
+ let subStr = el.getAttribute(DICT.BIND_ATTR);
271
+ let keyValsArr = subStr.split(';');
272
+ keyValsArr.forEach((keyValStr) => {
273
+ if (!keyValStr) {
274
+ return;
275
+ }
276
+ let kv = keyValStr.split(':').map((str) => str.trim());
277
+ let prop = kv[0];
278
+ let isAttr;
279
+
280
+ if (prop.indexOf(DICT.ATTR_BIND_PRFX) === 0) {
281
+ isAttr = true;
282
+ prop = prop.replace(DICT.ATTR_BIND_PRFX, '');
283
+ }
284
+ /** @type {String[]} */
285
+ let valKeysArr = kv[1].split(',').map((valKey) => {
286
+ return valKey.trim();
287
+ });
288
+ // Deep property:
289
+ let isDeep, parent, lastStep, dive;
290
+ if (prop.includes('.')) {
291
+ isDeep = true;
292
+ let propPath = prop.split('.');
293
+ dive = () => {
294
+ parent = el;
295
+ propPath.forEach((step, idx) => {
296
+ if (idx < propPath.length - 1) {
297
+ parent = parent[step];
298
+ } else {
299
+ lastStep = step;
300
+ }
301
+ });
302
+ };
303
+ dive();
304
+ }
305
+ for (let valKey of valKeysArr) {
306
+ fnCtx.sub(valKey, (val) => {
307
+ if (isAttr) {
308
+ if (val?.constructor === Boolean) {
309
+ val ? el.setAttribute(prop, '') : el.removeAttribute(prop);
310
+ } else {
311
+ el.setAttribute(prop, val);
312
+ }
313
+ } else if (isDeep) {
314
+ if (parent) {
315
+ parent[lastStep] = val;
316
+ } else {
317
+ // Custom element instances are not constructed properly at this time, so:
318
+ window.setTimeout(() => {
319
+ dive();
320
+ parent[lastStep] = val;
321
+ });
322
+ // TODO: investigate how to do it better ^^^
323
+ }
324
+ } else {
325
+ el[prop] = val;
326
+ }
327
+ });
328
+ }
329
+ });
330
+ el.removeAttribute(DICT.BIND_ATTR);
331
+ });
332
+ }
333
+
334
+ var PROCESSORS = [slotProcessor, refProcessor, domSetProcessor];
335
+
336
+ let autoTagsCount = 0;
337
+
338
+ class BaseComponent extends HTMLElement {
339
+ /**
340
+ * @param {String | DocumentFragment} [template]
341
+ * @param {Boolean} [shadow]
342
+ */
343
+ render(template, shadow = this.renderShadow) {
344
+ /** @type {DocumentFragment} */
345
+ let fr;
346
+ if (template || this.constructor['template']) {
347
+ if (this.constructor['template'] && !this.constructor['__tpl']) {
348
+ this.constructor['__tpl'] = document.createElement('template');
349
+ this.constructor['__tpl'].innerHTML = this.constructor['template'];
350
+ }
351
+ while (this.firstChild) {
352
+ this.firstChild.remove();
353
+ }
354
+ if (template?.constructor === DocumentFragment) {
355
+ fr = template;
356
+ } else if (template?.constructor === String) {
357
+ let tpl = document.createElement('template');
358
+ tpl.innerHTML = template;
359
+ // @ts-ignore
360
+ fr = tpl.content.cloneNode(true);
361
+ } else if (this.constructor['__tpl']) {
362
+ fr = this.constructor['__tpl'].content.cloneNode(true);
363
+ }
364
+ for (let fn of this.tplProcessors) {
365
+ fn(fr, this);
366
+ }
367
+ }
368
+ if (shadow) {
369
+ if (!this.shadowRoot) {
370
+ this.attachShadow({
371
+ mode: 'open',
372
+ });
373
+ }
374
+ fr && this.shadowRoot.appendChild(fr);
375
+ } else {
376
+ fr && this.appendChild(fr);
377
+ }
378
+ }
379
+
380
+ /** @param {(fr: DocumentFragment, fnCtx: any) => any} processorFn */
381
+ addTemplateProcessor(processorFn) {
382
+ this.tplProcessors.add(processorFn);
383
+ }
384
+
385
+ constructor() {
386
+ super();
387
+ /** @type {Object<string, any>} */
388
+ this.init$ = Object.create(null);
389
+ /** @type {Set<(fr: DocumentFragment, fnCtx: any) => any>} */
390
+ this.tplProcessors = new Set();
391
+ /** @type {Object<string, HTMLElement>} */
392
+ this.ref = Object.create(null);
393
+ this.allSubs = new Set();
394
+ this.pauseRender = false;
395
+ this.renderShadow = false;
396
+ this.readyToDestroy = true;
397
+ }
398
+
399
+ get autoCtxName() {
400
+ if (!this.__autoCtxName) {
401
+ this.__autoCtxName = UID.generate();
402
+ this.style.setProperty(DICT.CSS_CTX_PROP, `'${this.__autoCtxName}'`);
403
+ }
404
+ return this.__autoCtxName;
405
+ }
406
+
407
+ get cssCtxName() {
408
+ return this.getCssData(DICT.CSS_CTX_PROP, true);
409
+ }
410
+
411
+ get ctxName() {
412
+ return this.getAttribute(DICT.CTX_NAME_ATTR)?.trim() || this.cssCtxName || this.autoCtxName;
413
+ }
414
+
415
+ get localCtx() {
416
+ if (!this.__localCtx) {
417
+ this.__localCtx = Data.registerLocalCtx({});
418
+ }
419
+ return this.__localCtx;
420
+ }
421
+
422
+ get nodeCtx() {
423
+ return Data.getNamedCtx(this.ctxName, false) || Data.registerNamedCtx(this.ctxName, {});
424
+ }
425
+
426
+ /**
427
+ * @param {String} prop
428
+ * @param {any} fnCtx
429
+ */
430
+ static __parseProp(prop, fnCtx) {
431
+ /** @type {Data} */
432
+ let ctx;
433
+ /** @type {String} */
434
+ let name;
435
+ if (prop.startsWith(DICT.EXT_DATA_CTX_PRFX)) {
436
+ ctx = fnCtx.nodeCtx;
437
+ name = prop.replace(DICT.EXT_DATA_CTX_PRFX, '');
438
+ } else if (prop.includes(DICT.NAMED_DATA_CTX_SPLTR)) {
439
+ let pArr = prop.split(DICT.NAMED_DATA_CTX_SPLTR);
440
+ ctx = Data.getNamedCtx(pArr[0]);
441
+ name = pArr[1];
442
+ } else {
443
+ ctx = fnCtx.localCtx;
444
+ name = prop;
445
+ }
446
+ return {
447
+ ctx,
448
+ name,
449
+ };
450
+ }
451
+
452
+ /**
453
+ * @param {String} prop
454
+ * @param {(value: any) => void} handler
455
+ */
456
+ sub(prop, handler) {
457
+ let parsed = BaseComponent.__parseProp(prop, this);
458
+ this.allSubs.add(parsed.ctx.sub(parsed.name, handler));
459
+ }
460
+
461
+ /** @param {String} prop */
462
+ notify(prop) {
463
+ let parsed = BaseComponent.__parseProp(prop, this);
464
+ parsed.ctx.notify(parsed.name);
465
+ }
466
+
467
+ /** @param {String} prop */
468
+ has(prop) {
469
+ let parsed = BaseComponent.__parseProp(prop, this);
470
+ return parsed.ctx.has(parsed.name);
471
+ }
472
+
473
+ /**
474
+ * @param {String} prop
475
+ * @param {any} val
476
+ */
477
+ add(prop, val) {
478
+ let parsed = BaseComponent.__parseProp(prop, this);
479
+ parsed.ctx.add(parsed.name, val, false);
480
+ }
481
+
482
+ /** @param {Object<string, any>} obj */
483
+ add$(obj) {
484
+ for (let prop in obj) {
485
+ this.add(prop, obj[prop]);
486
+ }
487
+ }
488
+
489
+ get $() {
490
+ if (!this.__stateProxy) {
491
+ /** @type {Object<string, any>} */
492
+ let o = Object.create(null);
493
+ this.__stateProxy = new Proxy(o, {
494
+ set: (obj, /** @type {String} */ prop, val) => {
495
+ let parsed = BaseComponent.__parseProp(prop, this);
496
+ parsed.ctx.pub(parsed.name, val);
497
+ return true;
498
+ },
499
+ get: (obj, /** @type {String} */ prop) => {
500
+ let parsed = BaseComponent.__parseProp(prop, this);
501
+ return parsed.ctx.read(parsed.name);
502
+ },
503
+ });
504
+ }
505
+ return this.__stateProxy;
506
+ }
507
+
508
+ /** @param {Object<string, any>} kvObj */
509
+ set$(kvObj) {
510
+ for (let key in kvObj) {
511
+ this.$[key] = kvObj[key];
512
+ }
513
+ }
514
+
515
+ initCallback() {}
516
+
517
+ __initDataCtx() {
518
+ let attrDesc = this.constructor['__attrDesc'];
519
+ if (attrDesc) {
520
+ for (let prop of Object.values(attrDesc)) {
521
+ if (!Object.keys(this.init$).includes(prop)) {
522
+ this.init$[prop] = '';
523
+ }
524
+ }
525
+ }
526
+ for (let prop in this.init$) {
527
+ if (prop.startsWith(DICT.EXT_DATA_CTX_PRFX)) {
528
+ this.nodeCtx.add(prop.replace(DICT.EXT_DATA_CTX_PRFX, ''), this.init$[prop]);
529
+ } else if (prop.includes(DICT.NAMED_DATA_CTX_SPLTR)) {
530
+ let propArr = prop.split(DICT.NAMED_DATA_CTX_SPLTR);
531
+ let ctxName = propArr[0].trim();
532
+ let propName = propArr[1].trim();
533
+ if (ctxName && propName) {
534
+ let namedCtx = Data.getNamedCtx(ctxName, false);
535
+ if (!namedCtx) {
536
+ namedCtx = Data.registerNamedCtx(ctxName, {});
537
+ }
538
+ namedCtx.add(propName, this.init$[prop]);
539
+ }
540
+ } else {
541
+ this.localCtx.add(prop, this.init$[prop]);
542
+ }
543
+ }
544
+ this.__dataCtxInitialized = true;
545
+ }
546
+
547
+ connectedCallback() {
548
+ if (this.__disconnectTimeout) {
549
+ window.clearTimeout(this.__disconnectTimeout);
550
+ }
551
+ if (!this.connectedOnce) {
552
+ let ctxNameAttrVal = this.getAttribute(DICT.CTX_NAME_ATTR)?.trim();
553
+ if (ctxNameAttrVal) {
554
+ this.style.setProperty(DICT.CSS_CTX_PROP, `'${ctxNameAttrVal}'`);
555
+ }
556
+ this.__initDataCtx();
557
+ this.__initChildren = [...this.childNodes];
558
+ for (let proc of PROCESSORS) {
559
+ this.addTemplateProcessor(proc);
560
+ }
561
+ if (!this.pauseRender) {
562
+ this.render();
563
+ }
564
+ this.initCallback?.();
565
+ }
566
+ this.connectedOnce = true;
567
+ }
568
+
569
+ destroyCallback() {}
570
+
571
+ disconnectedCallback() {
572
+ this.dropCssDataCache();
573
+ if (!this.readyToDestroy) {
574
+ return;
575
+ }
576
+ if (this.__disconnectTimeout) {
577
+ window.clearTimeout(this.__disconnectTimeout);
578
+ }
579
+ this.__disconnectTimeout = window.setTimeout(() => {
580
+ this.destroyCallback();
581
+ for (let sub of this.allSubs) {
582
+ sub.remove();
583
+ this.allSubs.delete(sub);
584
+ }
585
+ for (let proc of this.tplProcessors) {
586
+ this.tplProcessors.delete(proc);
587
+ }
588
+ }, 100);
589
+ }
590
+
591
+ /**
592
+ * @param {String} [tagName]
593
+ * @param {Boolean} [isAlias]
594
+ */
595
+ static reg(tagName, isAlias = false) {
596
+ if (!tagName) {
597
+ autoTagsCount++;
598
+ tagName = `${DICT.AUTO_TAG_PRFX}-${autoTagsCount}`;
599
+ }
600
+ this.__tag = tagName;
601
+ if (window.customElements.get(tagName)) {
602
+ console.warn(`${tagName} - is already in "customElements" registry`);
603
+ return;
604
+ }
605
+ window.customElements.define(tagName, isAlias ? class extends this {} : this);
606
+ }
607
+
608
+ static get is() {
609
+ if (!this.__tag) {
610
+ this.reg();
611
+ }
612
+ return this.__tag;
613
+ }
614
+
615
+ /** @param {Object<string, string>} desc */
616
+ static bindAttributes(desc) {
617
+ this.observedAttributes = Object.keys(desc);
618
+ this.__attrDesc = desc;
619
+ }
620
+
621
+ attributeChangedCallback(name, oldVal, newVal) {
622
+ if (oldVal === newVal) {
623
+ return;
624
+ }
625
+ /** @type {String} */
626
+ let $prop = this.constructor['__attrDesc'][name];
627
+ if ($prop) {
628
+ if (this.__dataCtxInitialized) {
629
+ this.$[$prop] = newVal;
630
+ } else {
631
+ this.init$[$prop] = newVal;
632
+ }
633
+ } else {
634
+ this[name] = newVal;
635
+ }
636
+ }
637
+
638
+ /**
639
+ * @param {String} propName
640
+ * @param {Boolean} [silentCheck]
641
+ */
642
+ getCssData(propName, silentCheck = false) {
643
+ if (!this.__cssDataCache) {
644
+ this.__cssDataCache = Object.create(null);
645
+ }
646
+ if (!Object.keys(this.__cssDataCache).includes(propName)) {
647
+ if (!this.__computedStyle) {
648
+ this.__computedStyle = window.getComputedStyle(this);
649
+ }
650
+ let val = this.__computedStyle.getPropertyValue(propName).trim();
651
+ // Firefox doesn't transform string values into JSON format:
652
+ if (val.startsWith(`'`) && val.endsWith(`'`)) {
653
+ val = val.replace(/\'/g, '"');
654
+ }
655
+ try {
656
+ this.__cssDataCache[propName] = JSON.parse(val);
657
+ } catch (e) {
658
+ !silentCheck && console.warn(`CSS Data error: ${propName}`);
659
+ this.__cssDataCache[propName] = null;
660
+ }
661
+ }
662
+ return this.__cssDataCache[propName];
663
+ }
664
+
665
+ dropCssDataCache() {
666
+ this.__cssDataCache = null;
667
+ this.__computedStyle = null;
668
+ }
669
+
670
+ /**
671
+ * @param {String} propName
672
+ * @param {Function} [handler]
673
+ * @param {Boolean} [isAsync]
674
+ */
675
+ defineAccessor(propName, handler, isAsync) {
676
+ let localPropName = '__' + propName;
677
+ this[localPropName] = this[propName];
678
+ Object.defineProperty(this, propName, {
679
+ set: (val) => {
680
+ this[localPropName] = val;
681
+ if (isAsync) {
682
+ window.setTimeout(() => {
683
+ handler?.(val);
684
+ });
685
+ } else {
686
+ handler?.(val);
687
+ }
688
+ },
689
+ get: () => {
690
+ return this[localPropName];
691
+ },
692
+ });
693
+ this[propName] = this[localPropName];
694
+ }
695
+ }
696
+
697
+ const MSG_NAME = '[Typed State] Wrong property name: ';
698
+ const MSG_TYPE = '[Typed State] Wrong property type: ';
699
+
700
+ class TypedData {
701
+ /**
702
+ *
703
+ * @param {Object<string, {type: any, value: any}>} typedSchema
704
+ * @param {String} [ctxName]
705
+ */
706
+ constructor(typedSchema, ctxName) {
707
+ this.__typedSchema = typedSchema;
708
+ this.__ctxId = ctxName || UID.generate();
709
+ this.__schema = Object.keys(typedSchema).reduce((acc, key) => {
710
+ acc[key] = typedSchema[key].value;
711
+ return acc;
712
+ }, {});
713
+ this.__state = Data.registerNamedCtx(this.__ctxId, this.__schema);
714
+ }
715
+
716
+ /**
717
+ *
718
+ * @param {String} prop
719
+ * @param {*} value
720
+ */
721
+ setValue(prop, value) {
722
+ if (!this.__typedSchema.hasOwnProperty(prop)) {
723
+ console.warn(MSG_NAME + prop);
724
+ return;
725
+ }
726
+ if (value?.constructor !== this.__typedSchema[prop].type) {
727
+ console.warn(MSG_TYPE + prop);
728
+ return;
729
+ }
730
+ this.__state.pub(prop, value);
731
+ }
732
+
733
+ /**
734
+ *
735
+ * @param {Object<string, *>} updObj
736
+ */
737
+ setMultipleValues(updObj) {
738
+ for (let prop in updObj) {
739
+ this.setValue(prop, updObj[prop]);
740
+ }
741
+ }
742
+
743
+ /**
744
+ *
745
+ * @param {String} prop
746
+ */
747
+ getValue(prop) {
748
+ if (!this.__typedSchema.hasOwnProperty(prop)) {
749
+ console.warn(MSG_NAME + prop);
750
+ return undefined;
751
+ }
752
+ return this.__state.read(prop);
753
+ }
754
+
755
+ /**
756
+ *
757
+ * @param {String} prop
758
+ * @param {(newVal: any) => void} handler
759
+ */
760
+ subscribe(prop, handler) {
761
+ return this.__state.sub(prop, handler);
762
+ }
763
+
764
+ remove() {
765
+ this.__state.remove();
766
+ }
767
+ }
768
+
769
+ class TypedCollection {
770
+ /**
771
+ * @param {Object} options
772
+ * @param {Object<string, { type: any; value: any }>} options.typedSchema
773
+ * @param {String[]} [options.watchList]
774
+ * @param {(list: string[]) => void} [options.handler]
775
+ * @param {String} [options.ctxName]
776
+ */
777
+ constructor(options) {
778
+ /** @type {Object<string, { type: any; value: any }>} */
779
+ this.__typedSchema = options.typedSchema;
780
+ /** @type {String} */
781
+ this.__ctxId = options.ctxName || UID.generate();
782
+ /** @type {Data} */
783
+ this.__state = Data.registerNamedCtx(this.__ctxId, {});
784
+ /** @type {string[]} */
785
+ this.__watchList = options.watchList || [];
786
+ /** @type {(list: string[]) => void} */
787
+ this.__handler = options.handler || null;
788
+ this.__subsMap = Object.create(null);
789
+ /** @type {Set} */
790
+ this.__observers = new Set();
791
+ /** @type {Set<string>} */
792
+ this.__items = new Set();
793
+
794
+ let changeMap = Object.create(null);
795
+
796
+ this.__notifyObservers = (propName, ctxId) => {
797
+ if (this.__observeTimeout) {
798
+ window.clearTimeout(this.__observeTimeout);
799
+ }
800
+ if (!changeMap[propName]) {
801
+ changeMap[propName] = new Set();
802
+ }
803
+ changeMap[propName].add(ctxId);
804
+ this.__observeTimeout = window.setTimeout(() => {
805
+ this.__observers.forEach((handler) => {
806
+ handler({ ...changeMap });
807
+ });
808
+ changeMap = Object.create(null);
809
+ });
810
+ };
811
+ }
812
+
813
+ notify() {
814
+ if (this.__notifyTimeout) {
815
+ window.clearTimeout(this.__notifyTimeout);
816
+ }
817
+ this.__notifyTimeout = window.setTimeout(() => {
818
+ this.__handler?.([...this.__items]);
819
+ });
820
+ }
821
+
822
+ add(init) {
823
+ let item = new TypedData(this.__typedSchema);
824
+ for (let prop in init) {
825
+ item.setValue(prop, init[prop]);
826
+ }
827
+ this.__state.add(item.__ctxId, item);
828
+ this.__watchList.forEach((propName) => {
829
+ if (!this.__subsMap[item.__ctxId]) {
830
+ this.__subsMap[item.__ctxId] = [];
831
+ }
832
+ this.__subsMap[item.__ctxId].push(
833
+ item.subscribe(propName, () => {
834
+ this.__notifyObservers(propName, item.__ctxId);
835
+ })
836
+ );
837
+ });
838
+ this.__items.add(item.__ctxId);
839
+ this.notify();
840
+ return item;
841
+ }
842
+
843
+ /**
844
+ * @param {String} id
845
+ * @returns {TypedData}
846
+ */
847
+ read(id) {
848
+ return this.__state.read(id);
849
+ }
850
+
851
+ readProp(id, propName) {
852
+ let item = this.read(id);
853
+ return item.getValue(propName);
854
+ }
855
+
856
+ publishProp(id, propName, value) {
857
+ let item = this.read(id);
858
+ item.setValue(propName, value);
859
+ }
860
+
861
+ remove(id) {
862
+ this.__items.delete(id);
863
+ this.notify();
864
+ this.__state.pub(id, null);
865
+ delete this.__subsMap[id];
866
+ }
867
+
868
+ clearAll() {
869
+ this.__items.forEach((id) => {
870
+ this.remove(id);
871
+ });
872
+ }
873
+
874
+ /** @param {Function} handler */
875
+ observe(handler) {
876
+ this.__observers.add(handler);
877
+ }
878
+
879
+ /** @param {Function} handler */
880
+ unobserve(handler) {
881
+ this.__observers.delete(handler);
882
+ }
883
+
884
+ /**
885
+ * @param {(item: TypedData) => Boolean} checkFn
886
+ * @returns {String[]}
887
+ */
888
+ findItems(checkFn) {
889
+ let result = [];
890
+ this.__items.forEach((id) => {
891
+ let item = this.read(id);
892
+ if (checkFn(item)) {
893
+ result.push(id);
894
+ }
895
+ });
896
+ return result;
897
+ }
898
+
899
+ items() {
900
+ return [...this.__items];
901
+ }
902
+
903
+ destroy() {
904
+ this.__state.remove();
905
+ this.__observers = null;
906
+ for (let id in this.__subsMap) {
907
+ this.__subsMap[id].forEach((sub) => {
908
+ sub.remove();
909
+ });
910
+ delete this.__subsMap[id];
911
+ }
912
+ }
913
+ }
914
+
915
+ class AppRouter {
916
+
917
+ static _print(msg) {
918
+ console.warn(msg);
919
+ }
920
+
921
+ /**
922
+ *
923
+ * @param {String} title
924
+ */
925
+ static setDefaultTitle(title) {
926
+ this.defaultTitle = title;
927
+ }
928
+
929
+ /**
930
+ *
931
+ * @param {Object<string, {}>} map
932
+ */
933
+ static setRoutingMap(map) {
934
+ Object.assign(this.appMap, map);
935
+ for (let route in this.appMap) {
936
+ if (!this.defaultRoute && this.appMap[route].default === true) {
937
+ this.defaultRoute = route;
938
+ } else if (!this.errorRoute && this.appMap[route].error === true) {
939
+ this.errorRoute = route;
940
+ }
941
+ }
942
+ }
943
+
944
+ static set routingEventName(name) {
945
+ this.__routingEventName = name;
946
+ }
947
+
948
+ static get routingEventName() {
949
+ return this.__routingEventName || 'sym-on-route';
950
+ }
951
+
952
+ static readAddressBar() {
953
+ let result = {
954
+ route: null,
955
+ options: {},
956
+ };
957
+ let paramsArr = window.location.search.split(this.separator);
958
+ paramsArr.forEach((part) => {
959
+ if (part.includes('?')) {
960
+ result.route = part.replace('?', '');
961
+ } else if (part.includes('=')) {
962
+ let pair = part.split('=');
963
+ result.options[pair[0]] = decodeURI(pair[1]);
964
+ } else {
965
+ result.options[part] = true;
966
+ }
967
+ });
968
+ return result;
969
+ }
970
+
971
+ static notify() {
972
+ let routeBase = this.readAddressBar();
973
+ let routeScheme = this.appMap[routeBase.route];
974
+ if (routeScheme && routeScheme.title) {
975
+ document.title = routeScheme.title;
976
+ }
977
+ if (routeBase.route === null && this.defaultRoute) {
978
+ this.applyRoute(this.defaultRoute);
979
+ return;
980
+ } else if (!routeScheme && this.errorRoute) {
981
+ this.applyRoute(this.errorRoute);
982
+ return;
983
+ } else if (!routeScheme && this.defaultRoute) {
984
+ this.applyRoute(this.defaultRoute);
985
+ return;
986
+ } else if (!routeScheme) {
987
+ this._print(`Route "${routeBase.route}" not found...`);
988
+ return;
989
+ }
990
+ let event = new CustomEvent(AppRouter.routingEventName, {
991
+ detail: {
992
+ route: routeBase.route,
993
+ options: Object.assign(routeScheme || {}, routeBase.options),
994
+ },
995
+ });
996
+ window.dispatchEvent(event);
997
+ }
998
+
999
+ /**
1000
+ *
1001
+ * @param {String} route
1002
+ * @param {Object<string, *>} [options]
1003
+ */
1004
+ static reflect(route, options = {}) {
1005
+ let routeScheme = this.appMap[route];
1006
+ if (!routeScheme) {
1007
+ this._print('Wrong route: ' + route);
1008
+ return;
1009
+ }
1010
+ let routeStr = '?' + route;
1011
+ for (let prop in options) {
1012
+ if (options[prop] === true) {
1013
+ routeStr += this.separator + prop;
1014
+ } else {
1015
+ routeStr += this.separator + prop + '=' + `${options[prop]}`;
1016
+ }
1017
+ }
1018
+ let title = routeScheme.title || this.defaultTitle || '';
1019
+ window.history.pushState(null, title, routeStr);
1020
+ document.title = title;
1021
+ }
1022
+
1023
+ /**
1024
+ *
1025
+ * @param {String} route
1026
+ * @param {Object<string, *>} [options]
1027
+ */
1028
+ static applyRoute(route, options = {}) {
1029
+ this.reflect(route, options);
1030
+ this.notify();
1031
+ }
1032
+
1033
+ /**
1034
+ *
1035
+ * @param {String} char
1036
+ */
1037
+ static setSeparator(char) {
1038
+ this._separator = char;
1039
+ }
1040
+
1041
+ static get separator() {
1042
+ return this._separator || '&';
1043
+ }
1044
+
1045
+ /**
1046
+ *
1047
+ * @param {String} ctxName
1048
+ * @param {Object<string, {}>} routingMap
1049
+ * @returns {Data}
1050
+ */
1051
+ static createRouterData(ctxName, routingMap) {
1052
+ this.setRoutingMap(routingMap);
1053
+ let routeData = Data.registerNamedCtx(ctxName, {
1054
+ route: null,
1055
+ options: null,
1056
+ title: null,
1057
+ });
1058
+ window.addEventListener(this.routingEventName, (/** @type {CustomEvent} */ e) => {
1059
+ routeData.multiPub({
1060
+ route: e.detail.route,
1061
+ options: e.detail.options,
1062
+ title: e.detail.options?.title || this.defaultTitle || '',
1063
+ });
1064
+ });
1065
+ AppRouter.notify();
1066
+ return routeData;
1067
+ }
1068
+
1069
+ }
1070
+
1071
+ AppRouter.appMap = Object.create(null);
1072
+
1073
+ window.onpopstate = () => {
1074
+ AppRouter.notify();
1075
+ };
1076
+
1077
+ /**
1078
+ *
1079
+ * @typedef StyleMap
1080
+ * @type {Object<string, string | number>}
1081
+ */
1082
+
1083
+ /**
1084
+ *
1085
+ * @typedef AttrMap
1086
+ * @type {Object<string, string | number | boolean>}
1087
+ */
1088
+
1089
+ /**
1090
+ *
1091
+ * @typedef PropMap
1092
+ * @type {Object<string, *>}
1093
+ */
1094
+
1095
+ /**
1096
+ *
1097
+ * @param {*} el HTMLElement
1098
+ * @param {StyleMap} styleMap
1099
+ */
1100
+ function applyStyles(el, styleMap) {
1101
+ for (let prop in styleMap) {
1102
+ if (prop.includes('-')) {
1103
+ el.style.setProperty(prop, styleMap[prop]);
1104
+ } else {
1105
+ el.style[prop] = styleMap[prop];
1106
+ }
1107
+ }
1108
+ }
1109
+ /**
1110
+ *
1111
+ * @param {*} el HTMLElement
1112
+ * @param {AttrMap} attrMap
1113
+ */
1114
+ function applyAttributes(el, attrMap) {
1115
+ for (let attrName in attrMap) {
1116
+ if (attrMap[attrName].constructor === Boolean) {
1117
+ if (attrMap[attrName]) {
1118
+ el.setAttribute(attrName, '');
1119
+ } else {
1120
+ el.removeAttribute(attrName);
1121
+ }
1122
+ } else {
1123
+ el.setAttribute(attrName, attrMap[attrName]);
1124
+ }
1125
+ }
1126
+ }
1127
+ /**
1128
+ *
1129
+ * @typedef ElementDescriptor
1130
+ * @type {{
1131
+ * tag?: String,
1132
+ * attributes?: AttrMap,
1133
+ * styles?: StyleMap,
1134
+ * properties?: PropMap,
1135
+ * processors?: Function[],
1136
+ * children?: ElementDescriptor[],
1137
+ * }}
1138
+ */
1139
+
1140
+ /**
1141
+ *
1142
+ * @param {ElementDescriptor} [desc]
1143
+ * @returns {HTMLElement}
1144
+ */
1145
+ function create(desc = {tag: 'div'}) {
1146
+ let el = document.createElement(desc.tag);
1147
+ if (desc.attributes) {
1148
+ applyAttributes(el, desc.attributes);
1149
+ }
1150
+ if (desc.styles) {
1151
+ applyStyles(el, desc.styles);
1152
+ }
1153
+ if (desc.properties) {
1154
+ for (let prop in desc.properties) {
1155
+ el[prop] = desc.properties[prop];
1156
+ }
1157
+ }
1158
+ if (desc.processors) {
1159
+ desc.processors.forEach((fn) => {
1160
+ fn(el);
1161
+ });
1162
+ }
1163
+ if (desc.children) {
1164
+ desc.children.forEach((desc) => {
1165
+ let child = create(desc);
1166
+ el.appendChild(child);
1167
+ });
1168
+ }
1169
+ return el;
1170
+ }
1171
+
1172
+ const READY_EVENT_NAME = 'idb-store-ready';
1173
+
1174
+ const DEFAULT_DB_NAME = `symbiote-db`;
1175
+ const UPD_EVENT_PREFIX = `symbiote-idb-update_`;
1176
+
1177
+ class DbInstance {
1178
+ _notifyWhenReady(event = null) {
1179
+ window.dispatchEvent(
1180
+ new CustomEvent(READY_EVENT_NAME, {
1181
+ detail: {
1182
+ dbName: this.name,
1183
+ storeName: this.storeName,
1184
+ event,
1185
+ },
1186
+ })
1187
+ );
1188
+ }
1189
+
1190
+ get _updEventName() {
1191
+ return UPD_EVENT_PREFIX + this.name;
1192
+ }
1193
+
1194
+ /** @param {any} key */
1195
+ _getUpdateEvent(key) {
1196
+ return new CustomEvent(this._updEventName, {
1197
+ detail: {
1198
+ key: this.name,
1199
+ newValue: key,
1200
+ },
1201
+ });
1202
+ }
1203
+
1204
+ _notifySubscribers(key) {
1205
+ window.localStorage.removeItem(this.name);
1206
+ window.localStorage.setItem(this.name, key);
1207
+ window.dispatchEvent(this._getUpdateEvent(key));
1208
+ }
1209
+
1210
+ /**
1211
+ * @param {String} dbName
1212
+ * @param {String} storeName
1213
+ */
1214
+ constructor(dbName, storeName) {
1215
+ this.name = dbName;
1216
+ this.storeName = storeName;
1217
+ this.version = 1;
1218
+ this.request = window.indexedDB.open(this.name, this.version);
1219
+ this.request.onupgradeneeded = (e) => {
1220
+ this.db = e.target['result'];
1221
+ this.objStore = this.db.createObjectStore(storeName, {
1222
+ keyPath: '_key',
1223
+ });
1224
+ this.objStore.transaction.oncomplete = (ev) => {
1225
+ this._notifyWhenReady(ev);
1226
+ };
1227
+ };
1228
+ this.request.onsuccess = (e) => {
1229
+ // @ts-ignore
1230
+ this.db = e.target.result;
1231
+ this._notifyWhenReady(e);
1232
+ };
1233
+ this.request.onerror = (e) => {
1234
+ console.error(e);
1235
+ };
1236
+ this._subscribtionsMap = {};
1237
+ this._updateHandler = (/** @type {StorageEvent} */ e) => {
1238
+ if (e.key === this.name && this._subscribtionsMap[e.newValue]) {
1239
+ /** @type {Set<Function>} */
1240
+ let set = this._subscribtionsMap[e.newValue];
1241
+ set.forEach(async (callback) => {
1242
+ callback(await this.read(e.newValue));
1243
+ });
1244
+ }
1245
+ };
1246
+ this._localUpdateHandler = (e) => {
1247
+ this._updateHandler(e.detail);
1248
+ };
1249
+ window.addEventListener('storage', this._updateHandler);
1250
+ window.addEventListener(this._updEventName, this._localUpdateHandler);
1251
+ }
1252
+
1253
+ /** @param {String} key */
1254
+ read(key) {
1255
+ let tx = this.db.transaction(this.storeName, 'readwrite');
1256
+ let request = tx.objectStore(this.storeName).get(key);
1257
+ return new Promise((resolve, reject) => {
1258
+ request.onsuccess = (e) => {
1259
+ if (e.target.result?._value) {
1260
+ resolve(e.target.result._value);
1261
+ } else {
1262
+ resolve(null);
1263
+ console.warn(`IDB: cannot read "${key}"`);
1264
+ }
1265
+ };
1266
+ request.onerror = (e) => {
1267
+ reject(e);
1268
+ };
1269
+ });
1270
+ }
1271
+
1272
+ /**
1273
+ * @param {String} key
1274
+ * @param {any} value
1275
+ * @param {Boolean} [silent]
1276
+ */
1277
+ write(key, value, silent = false) {
1278
+ let data = {
1279
+ _key: key,
1280
+ _value: value,
1281
+ };
1282
+ let tx = this.db.transaction(this.storeName, 'readwrite');
1283
+ let request = tx.objectStore(this.storeName).put(data);
1284
+ return new Promise((resolve, reject) => {
1285
+ request.onsuccess = (e) => {
1286
+ if (!silent) {
1287
+ this._notifySubscribers(key);
1288
+ }
1289
+ resolve(e.target.result);
1290
+ };
1291
+ request.onerror = (e) => {
1292
+ reject(e);
1293
+ };
1294
+ });
1295
+ }
1296
+
1297
+ /**
1298
+ * @param {String} key
1299
+ * @param {Boolean} [silent]
1300
+ */
1301
+ delete(key, silent = false) {
1302
+ let tx = this.db.transaction(this.storeName, 'readwrite');
1303
+ let request = tx.objectStore(this.storeName).delete(key);
1304
+ return new Promise((resolve, reject) => {
1305
+ request.onsuccess = (e) => {
1306
+ if (!silent) {
1307
+ this._notifySubscribers(key);
1308
+ }
1309
+ resolve(e);
1310
+ };
1311
+ request.onerror = (e) => {
1312
+ reject(e);
1313
+ };
1314
+ });
1315
+ }
1316
+
1317
+ getAll() {
1318
+ let tx = this.db.transaction(this.storeName, 'readwrite');
1319
+ let request = tx.objectStore(this.storeName).getAll();
1320
+ return new Promise((resolve, reject) => {
1321
+ request.onsuccess = (e) => {
1322
+ let all = e.target.result;
1323
+ resolve(
1324
+ all.map((obj) => {
1325
+ return obj._value;
1326
+ })
1327
+ );
1328
+ };
1329
+ request.onerror = (e) => {
1330
+ reject(e);
1331
+ };
1332
+ });
1333
+ }
1334
+
1335
+ /**
1336
+ * @param {String} key
1337
+ * @param {Function} callback
1338
+ */
1339
+ subscribe(key, callback) {
1340
+ if (!this._subscribtionsMap[key]) {
1341
+ this._subscribtionsMap[key] = new Set();
1342
+ }
1343
+ /** @type {Set} */
1344
+ let set = this._subscribtionsMap[key];
1345
+ set.add(callback);
1346
+ return {
1347
+ remove: () => {
1348
+ set.delete(callback);
1349
+ if (!set.size) {
1350
+ delete this._subscribtionsMap[key];
1351
+ }
1352
+ },
1353
+ };
1354
+ }
1355
+
1356
+ stop() {
1357
+ window.removeEventListener('storage', this._updateHandler);
1358
+ this.__subscribtionsMap = null;
1359
+ IDB.clear(this.name);
1360
+ }
1361
+ }
1362
+
1363
+ class IDB {
1364
+ static get readyEventName() {
1365
+ return READY_EVENT_NAME;
1366
+ }
1367
+
1368
+ /**
1369
+ * @param {String} dbName
1370
+ * @param {String} storeName
1371
+ * @returns {DbInstance}
1372
+ */
1373
+ static open(dbName = DEFAULT_DB_NAME, storeName = 'store') {
1374
+ let key = `${dbName}/${storeName}`;
1375
+ if (!this._reg[key]) {
1376
+ this._reg[key] = new DbInstance(dbName, storeName);
1377
+ }
1378
+ return this._reg[key];
1379
+ }
1380
+
1381
+ /** @param {String} dbName */
1382
+ static clear(dbName) {
1383
+ window.indexedDB.deleteDatabase(dbName);
1384
+ for (let key in this._reg) {
1385
+ if (key.split('/')[0] === dbName) {
1386
+ delete this._reg[key];
1387
+ }
1388
+ }
1389
+ }
1390
+ }
1391
+
1392
+ IDB._reg = Object.create(null);
1393
+
1394
+ export { AppRouter, BaseComponent, Data, IDB, TypedCollection, TypedData, UID, applyAttributes, applyStyles, create };