@symbiotejs/symbiote 1.4.2 → 1.4.3

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,155 @@
1
+ import { Data } from './Data.js';
2
+
3
+ export class AppRouter {
4
+ /** @private */
5
+ static _print(msg) {
6
+ console.warn(msg);
7
+ }
8
+
9
+ /** @param {String} title */
10
+ static setDefaultTitle(title) {
11
+ this.defaultTitle = title;
12
+ }
13
+
14
+ /** @param {Object<string, {}>} map */
15
+ static setRoutingMap(map) {
16
+ Object.assign(this.appMap, map);
17
+ for (let route in this.appMap) {
18
+ if (!this.defaultRoute && this.appMap[route].default === true) {
19
+ this.defaultRoute = route;
20
+ } else if (!this.errorRoute && this.appMap[route].error === true) {
21
+ this.errorRoute = route;
22
+ }
23
+ }
24
+ }
25
+
26
+ /** @param {String} name */
27
+ static set routingEventName(name) {
28
+ /** @private */
29
+ this.__routingEventName = name;
30
+ }
31
+
32
+ /** @returns {String} */
33
+ static get routingEventName() {
34
+ return this.__routingEventName || 'sym-on-route';
35
+ }
36
+
37
+ static readAddressBar() {
38
+ let result = {
39
+ route: null,
40
+ options: {},
41
+ };
42
+ let paramsArr = window.location.search.split(this.separator);
43
+ paramsArr.forEach((part) => {
44
+ if (part.includes('?')) {
45
+ result.route = part.replace('?', '');
46
+ } else if (part.includes('=')) {
47
+ let pair = part.split('=');
48
+ result.options[pair[0]] = decodeURI(pair[1]);
49
+ } else {
50
+ result.options[part] = true;
51
+ }
52
+ });
53
+ return result;
54
+ }
55
+
56
+ static notify() {
57
+ let routeBase = this.readAddressBar();
58
+ let routeScheme = this.appMap[routeBase.route];
59
+ if (routeScheme && routeScheme.title) {
60
+ document.title = routeScheme.title;
61
+ }
62
+ if (routeBase.route === null && this.defaultRoute) {
63
+ this.applyRoute(this.defaultRoute);
64
+ return;
65
+ } else if (!routeScheme && this.errorRoute) {
66
+ this.applyRoute(this.errorRoute);
67
+ return;
68
+ } else if (!routeScheme && this.defaultRoute) {
69
+ this.applyRoute(this.defaultRoute);
70
+ return;
71
+ } else if (!routeScheme) {
72
+ this._print(`Route "${routeBase.route}" not found...`);
73
+ return;
74
+ }
75
+ let event = new CustomEvent(AppRouter.routingEventName, {
76
+ detail: {
77
+ route: routeBase.route,
78
+ options: Object.assign(routeScheme || {}, routeBase.options),
79
+ },
80
+ });
81
+ window.dispatchEvent(event);
82
+ }
83
+
84
+ /**
85
+ * @param {String} route
86
+ * @param {Object<string, any>} [options]
87
+ */
88
+ static reflect(route, options = {}) {
89
+ let routeScheme = this.appMap[route];
90
+ if (!routeScheme) {
91
+ this._print('Wrong route: ' + route);
92
+ return;
93
+ }
94
+ let routeStr = '?' + route;
95
+ for (let prop in options) {
96
+ if (options[prop] === true) {
97
+ routeStr += this.separator + prop;
98
+ } else {
99
+ routeStr += this.separator + prop + '=' + `${options[prop]}`;
100
+ }
101
+ }
102
+ let title = routeScheme.title || this.defaultTitle || '';
103
+ window.history.pushState(null, title, routeStr);
104
+ document.title = title;
105
+ }
106
+
107
+ /**
108
+ * @param {String} route
109
+ * @param {Object<string, any>} [options]
110
+ */
111
+ static applyRoute(route, options = {}) {
112
+ this.reflect(route, options);
113
+ this.notify();
114
+ }
115
+
116
+ /** @param {String} char */
117
+ static setSeparator(char) {
118
+ /** @private */
119
+ this._separator = char;
120
+ }
121
+
122
+ /** @returns {String} */
123
+ static get separator() {
124
+ return this._separator || '&';
125
+ }
126
+
127
+ /**
128
+ * @param {String} ctxName
129
+ * @param {Object<string, {}>} routingMap
130
+ * @returns {Data}
131
+ */
132
+ static createRouterData(ctxName, routingMap) {
133
+ this.setRoutingMap(routingMap);
134
+ let routeData = Data.registerNamedCtx(ctxName, {
135
+ route: null,
136
+ options: null,
137
+ title: null,
138
+ });
139
+ window.addEventListener(this.routingEventName, (/** @type {CustomEvent} */ e) => {
140
+ routeData.multiPub({
141
+ route: e.detail.route,
142
+ options: e.detail.options,
143
+ title: e.detail.options?.title || this.defaultTitle || '',
144
+ });
145
+ });
146
+ AppRouter.notify();
147
+ return routeData;
148
+ }
149
+ }
150
+
151
+ AppRouter.appMap = Object.create(null);
152
+
153
+ window.onpopstate = () => {
154
+ AppRouter.notify();
155
+ };
@@ -0,0 +1,437 @@
1
+ import { Data } from './Data.js';
2
+ import { DICT } from './dictionary.js';
3
+ import { UID } from '../utils/UID.js';
4
+
5
+ import PROCESSORS from './tpl-processors.js';
6
+
7
+ let autoTagsCount = 0;
8
+
9
+ export class BaseComponent extends HTMLElement {
10
+ initCallback() {}
11
+
12
+ /** @private */
13
+ __initCallback() {
14
+ if (this.__initialized) {
15
+ return;
16
+ }
17
+ /** @private */
18
+ this.__initialized = true;
19
+ this.initCallback?.();
20
+ }
21
+
22
+ /** @type {String} */
23
+ static template;
24
+
25
+ /**
26
+ * @param {String | DocumentFragment} [template]
27
+ * @param {Boolean} [shadow]
28
+ */
29
+ render(template, shadow = this.renderShadow) {
30
+ /** @type {DocumentFragment} */
31
+ let fr;
32
+ if (template || this.constructor['template']) {
33
+ if (this.constructor['template'] && !this.constructor['__tpl']) {
34
+ this.constructor['__tpl'] = document.createElement('template');
35
+ this.constructor['__tpl'].innerHTML = this.constructor['template'];
36
+ }
37
+ while (this.firstChild) {
38
+ this.firstChild.remove();
39
+ }
40
+ if (template?.constructor === DocumentFragment) {
41
+ fr = template;
42
+ } else if (template?.constructor === String) {
43
+ let tpl = document.createElement('template');
44
+ tpl.innerHTML = template;
45
+ // @ts-ignore
46
+ fr = tpl.content.cloneNode(true);
47
+ } else if (this.constructor['__tpl']) {
48
+ fr = this.constructor['__tpl'].content.cloneNode(true);
49
+ }
50
+ for (let fn of this.tplProcessors) {
51
+ fn(fr, this);
52
+ }
53
+ }
54
+ if ((shadow || this.constructor['__shadowStylesUrl']) && !this.shadowRoot) {
55
+ this.attachShadow({
56
+ mode: 'open',
57
+ });
58
+ }
59
+
60
+ // for the possible asynchronous call:
61
+ let addFr = () => {
62
+ fr && ((shadow && this.shadowRoot.appendChild(fr)) || this.appendChild(fr));
63
+ this.__initCallback();
64
+ };
65
+
66
+ if (this.constructor['__shadowStylesUrl']) {
67
+ shadow = true; // is needed for cases when Shadow DOM was created manually for some other purposes
68
+ let styleLink = document.createElement('link');
69
+ styleLink.rel = 'stylesheet';
70
+ styleLink.href = this.constructor['__shadowStylesUrl'];
71
+ styleLink.onload = addFr;
72
+ this.shadowRoot.prepend(styleLink); // the link shoud be added before the other template elements
73
+ } else {
74
+ addFr();
75
+ }
76
+ }
77
+
78
+ /**
79
+ * @template {BaseComponent} T
80
+ * @param {(fr: DocumentFragment, fnCtx: T) => void} processorFn
81
+ */
82
+ addTemplateProcessor(processorFn) {
83
+ this.tplProcessors.add(processorFn);
84
+ }
85
+
86
+ constructor() {
87
+ super();
88
+ /** @type {Object<string, unknown>} */
89
+ this.init$ = Object.create(null);
90
+
91
+ /** @type {Set<(fr: DocumentFragment, fnCtx: unknown) => void>} */
92
+ this.tplProcessors = new Set();
93
+ /** @type {Object<string, unknown>} */
94
+ this.ref = Object.create(null);
95
+ this.allSubs = new Set();
96
+ /** @type {Boolean} */
97
+ this.pauseRender = false;
98
+ /** @type {Boolean} */
99
+ this.renderShadow = false;
100
+ /** @type {Boolean} */
101
+ this.readyToDestroy = true;
102
+ }
103
+
104
+ /** @returns {String} */
105
+ get autoCtxName() {
106
+ if (!this.__autoCtxName) {
107
+ /** @private */
108
+ this.__autoCtxName = UID.generate();
109
+ this.style.setProperty(DICT.CSS_CTX_PROP, `'${this.__autoCtxName}'`);
110
+ }
111
+ return this.__autoCtxName;
112
+ }
113
+
114
+ /** @returns {String} */
115
+ get cssCtxName() {
116
+ return this.getCssData(DICT.CSS_CTX_PROP, true);
117
+ }
118
+
119
+ /** @returns {String} */
120
+ get ctxName() {
121
+ return this.getAttribute(DICT.CTX_NAME_ATTR)?.trim() || this.cssCtxName || this.autoCtxName;
122
+ }
123
+
124
+ /** @returns {Data} */
125
+ get localCtx() {
126
+ if (!this.__localCtx) {
127
+ /** @private */
128
+ this.__localCtx = Data.registerLocalCtx({});
129
+ }
130
+ return this.__localCtx;
131
+ }
132
+
133
+ /** @returns {Data} */
134
+ get nodeCtx() {
135
+ return Data.getNamedCtx(this.ctxName, false) || Data.registerNamedCtx(this.ctxName, {});
136
+ }
137
+
138
+ /**
139
+ * @private
140
+ * @template {BaseComponent} T
141
+ * @param {String} prop
142
+ * @param {T} fnCtx
143
+ */
144
+ static __parseProp(prop, fnCtx) {
145
+ /** @type {Data} */
146
+ let ctx;
147
+ /** @type {String} */
148
+ let name;
149
+ if (prop.startsWith(DICT.EXT_DATA_CTX_PRFX)) {
150
+ ctx = fnCtx.nodeCtx;
151
+ name = prop.replace(DICT.EXT_DATA_CTX_PRFX, '');
152
+ } else if (prop.includes(DICT.NAMED_DATA_CTX_SPLTR)) {
153
+ let pArr = prop.split(DICT.NAMED_DATA_CTX_SPLTR);
154
+ ctx = Data.getNamedCtx(pArr[0]);
155
+ name = pArr[1];
156
+ } else {
157
+ ctx = fnCtx.localCtx;
158
+ name = prop;
159
+ }
160
+ return {
161
+ ctx,
162
+ name,
163
+ };
164
+ }
165
+
166
+ /**
167
+ * @template T
168
+ * @param {String} prop
169
+ * @param {(value: T) => void} handler
170
+ */
171
+ sub(prop, handler) {
172
+ let parsed = BaseComponent.__parseProp(prop, this);
173
+ this.allSubs.add(parsed.ctx.sub(parsed.name, handler));
174
+ }
175
+
176
+ /** @param {String} prop */
177
+ notify(prop) {
178
+ let parsed = BaseComponent.__parseProp(prop, this);
179
+ parsed.ctx.notify(parsed.name);
180
+ }
181
+
182
+ /** @param {String} prop */
183
+ has(prop) {
184
+ let parsed = BaseComponent.__parseProp(prop, this);
185
+ return parsed.ctx.has(parsed.name);
186
+ }
187
+
188
+ /**
189
+ * @template T
190
+ * @param {String} prop
191
+ * @param {T} val
192
+ */
193
+ add(prop, val) {
194
+ let parsed = BaseComponent.__parseProp(prop, this);
195
+ parsed.ctx.add(parsed.name, val, false);
196
+ }
197
+
198
+ /**
199
+ * @template T
200
+ * @param {Object<string, T>} obj
201
+ */
202
+ add$(obj) {
203
+ for (let prop in obj) {
204
+ this.add(prop, obj[prop]);
205
+ }
206
+ }
207
+
208
+ get $() {
209
+ if (!this.__stateProxy) {
210
+ /** @type {Object<string, any>} */
211
+ let o = Object.create(null);
212
+ /** @private */
213
+ this.__stateProxy = new Proxy(o, {
214
+ set: (obj, /** @type {String} */ prop, val) => {
215
+ let parsed = BaseComponent.__parseProp(prop, this);
216
+ parsed.ctx.pub(parsed.name, val);
217
+ return true;
218
+ },
219
+ get: (obj, /** @type {String} */ prop) => {
220
+ let parsed = BaseComponent.__parseProp(prop, this);
221
+ return parsed.ctx.read(parsed.name);
222
+ },
223
+ });
224
+ }
225
+ return this.__stateProxy;
226
+ }
227
+
228
+ /** @param {Object<string, any>} kvObj */
229
+ set$(kvObj) {
230
+ for (let key in kvObj) {
231
+ this.$[key] = kvObj[key];
232
+ }
233
+ }
234
+
235
+ /** @private */
236
+ __initDataCtx() {
237
+ let attrDesc = this.constructor['__attrDesc'];
238
+ if (attrDesc) {
239
+ for (let prop of Object.values(attrDesc)) {
240
+ if (!Object.keys(this.init$).includes(prop)) {
241
+ this.init$[prop] = '';
242
+ }
243
+ }
244
+ }
245
+ for (let prop in this.init$) {
246
+ if (prop.startsWith(DICT.EXT_DATA_CTX_PRFX)) {
247
+ this.nodeCtx.add(prop.replace(DICT.EXT_DATA_CTX_PRFX, ''), this.init$[prop]);
248
+ } else if (prop.includes(DICT.NAMED_DATA_CTX_SPLTR)) {
249
+ let propArr = prop.split(DICT.NAMED_DATA_CTX_SPLTR);
250
+ let ctxName = propArr[0].trim();
251
+ let propName = propArr[1].trim();
252
+ if (ctxName && propName) {
253
+ let namedCtx = Data.getNamedCtx(ctxName, false);
254
+ if (!namedCtx) {
255
+ namedCtx = Data.registerNamedCtx(ctxName, {});
256
+ }
257
+ namedCtx.add(propName, this.init$[prop]);
258
+ }
259
+ } else {
260
+ this.localCtx.add(prop, this.init$[prop]);
261
+ }
262
+ }
263
+ this.__dataCtxInitialized = true;
264
+ }
265
+
266
+ connectedCallback() {
267
+ if (this.__disconnectTimeout) {
268
+ window.clearTimeout(this.__disconnectTimeout);
269
+ }
270
+ if (!this.connectedOnce) {
271
+ let ctxNameAttrVal = this.getAttribute(DICT.CTX_NAME_ATTR)?.trim();
272
+ if (ctxNameAttrVal) {
273
+ this.style.setProperty(DICT.CSS_CTX_PROP, `'${ctxNameAttrVal}'`);
274
+ }
275
+ this.__initDataCtx();
276
+ for (let proc of PROCESSORS) {
277
+ this.addTemplateProcessor(proc);
278
+ }
279
+ if (!this.pauseRender) {
280
+ this.render();
281
+ }
282
+ }
283
+ this.connectedOnce = true;
284
+ }
285
+
286
+ destroyCallback() {}
287
+
288
+ disconnectedCallback() {
289
+ this.dropCssDataCache();
290
+ if (!this.readyToDestroy) {
291
+ return;
292
+ }
293
+ if (this.__disconnectTimeout) {
294
+ window.clearTimeout(this.__disconnectTimeout);
295
+ }
296
+ /** @private */
297
+ this.__disconnectTimeout = window.setTimeout(() => {
298
+ this.destroyCallback();
299
+ for (let sub of this.allSubs) {
300
+ sub.remove();
301
+ this.allSubs.delete(sub);
302
+ }
303
+ for (let proc of this.tplProcessors) {
304
+ this.tplProcessors.delete(proc);
305
+ }
306
+ }, 100);
307
+ }
308
+
309
+ /**
310
+ * @param {String} [tagName]
311
+ * @param {Boolean} [isAlias]
312
+ */
313
+ static reg(tagName, isAlias = false) {
314
+ if (!tagName) {
315
+ autoTagsCount++;
316
+ tagName = `${DICT.AUTO_TAG_PRFX}-${autoTagsCount}`;
317
+ }
318
+ /** @private */
319
+ this.__tag = tagName;
320
+ if (window.customElements.get(tagName)) {
321
+ console.warn(`${tagName} - is already in "customElements" registry`);
322
+ return;
323
+ }
324
+ window.customElements.define(tagName, isAlias ? class extends this {} : this);
325
+ }
326
+
327
+ static get is() {
328
+ if (!this.__tag) {
329
+ this.reg();
330
+ }
331
+ return this.__tag;
332
+ }
333
+
334
+ /** @param {Object<string, string>} desc */
335
+ static bindAttributes(desc) {
336
+ this.observedAttributes = Object.keys(desc);
337
+ /** @private */
338
+ this.__attrDesc = desc;
339
+ }
340
+
341
+ attributeChangedCallback(name, oldVal, newVal) {
342
+ if (oldVal === newVal) {
343
+ return;
344
+ }
345
+ /** @type {String} */
346
+ let $prop = this.constructor['__attrDesc'][name];
347
+ if ($prop) {
348
+ if (this.__dataCtxInitialized) {
349
+ this.$[$prop] = newVal;
350
+ } else {
351
+ this.init$[$prop] = newVal;
352
+ }
353
+ } else {
354
+ this[name] = newVal;
355
+ }
356
+ }
357
+
358
+ /**
359
+ * @param {String} propName
360
+ * @param {Boolean} [silentCheck]
361
+ */
362
+ getCssData(propName, silentCheck = false) {
363
+ if (!this.__cssDataCache) {
364
+ /** @private */
365
+ this.__cssDataCache = Object.create(null);
366
+ }
367
+ if (!Object.keys(this.__cssDataCache).includes(propName)) {
368
+ if (!this.__computedStyle) {
369
+ /** @private */
370
+ this.__computedStyle = window.getComputedStyle(this);
371
+ }
372
+ let val = this.__computedStyle.getPropertyValue(propName).trim();
373
+ // Firefox doesn't transform string values into JSON format:
374
+ if (val.startsWith(`'`) && val.endsWith(`'`)) {
375
+ val = val.replace(/\'/g, '"');
376
+ }
377
+ try {
378
+ this.__cssDataCache[propName] = JSON.parse(val);
379
+ } catch (e) {
380
+ !silentCheck && console.warn(`CSS Data error: ${propName}`);
381
+ this.__cssDataCache[propName] = null;
382
+ }
383
+ }
384
+ return this.__cssDataCache[propName];
385
+ }
386
+
387
+ /**
388
+ * @param {String} propName
389
+ * @param {Boolean} [external]
390
+ * @returns {String}
391
+ */
392
+ bindCssData(propName, external = true) {
393
+ let stateName = (external ? DICT.EXT_DATA_CTX_PRFX : '') + propName;
394
+ this.add(stateName, this.getCssData(propName, true));
395
+ return stateName;
396
+ }
397
+
398
+ dropCssDataCache() {
399
+ this.__cssDataCache = null;
400
+ this.__computedStyle = null;
401
+ }
402
+
403
+ /**
404
+ * @param {String} propName
405
+ * @param {Function} [handler]
406
+ * @param {Boolean} [isAsync]
407
+ */
408
+ defineAccessor(propName, handler, isAsync) {
409
+ let localPropName = '__' + propName;
410
+ this[localPropName] = this[propName];
411
+ Object.defineProperty(this, propName, {
412
+ set: (val) => {
413
+ this[localPropName] = val;
414
+ if (isAsync) {
415
+ window.setTimeout(() => {
416
+ handler?.(val);
417
+ });
418
+ } else {
419
+ handler?.(val);
420
+ }
421
+ },
422
+ get: () => {
423
+ return this[localPropName];
424
+ },
425
+ });
426
+ this[propName] = this[localPropName];
427
+ }
428
+
429
+ /** @param {String} cssTxt */
430
+ static set shadowStyles(cssTxt) {
431
+ let styleBlob = new Blob([cssTxt], {
432
+ type: 'text/css',
433
+ });
434
+ /** @private */
435
+ this.__shadowStylesUrl = URL.createObjectURL(styleBlob);
436
+ }
437
+ }