native-document 1.0.10 → 1.0.12
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.
- package/dist/native-document.dev.js +2671 -0
- package/dist/native-document.min.js +1 -0
- package/docs/anchor.md +212 -0
- package/docs/conditional-rendering.md +628 -0
- package/docs/contributing.md +51 -0
- package/docs/core-concepts.md +513 -0
- package/docs/elements.md +383 -0
- package/docs/getting-started.md +403 -0
- package/docs/lifecycle-events.md +106 -0
- package/docs/memory-management.md +90 -0
- package/docs/observables.md +265 -0
- package/docs/routing.md +817 -0
- package/docs/state-management.md +423 -0
- package/docs/validation.md +193 -0
- package/elements.js +3 -1
- package/index.js +1 -0
- package/package.json +1 -1
- package/readme.md +189 -425
- package/router.js +2 -0
- package/src/data/MemoryManager.js +15 -5
- package/src/data/Observable.js +15 -2
- package/src/data/ObservableChecker.js +3 -0
- package/src/data/ObservableItem.js +4 -0
- package/src/data/Store.js +6 -6
- package/src/elements/anchor.js +1 -1
- package/src/router/Router.js +13 -13
- package/src/router/link.js +8 -5
- package/src/utils/prototypes.js +13 -0
- package/src/utils/validator.js +16 -0
- package/src/wrappers/AttributesWrapper.js +11 -1
- package/src/wrappers/HtmlElementWrapper.js +4 -1
|
@@ -0,0 +1,2671 @@
|
|
|
1
|
+
var NativeDocument = (function (exports) {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
function eventWrapper(element, name, callback) {
|
|
5
|
+
element.addEventListener(name, callback);
|
|
6
|
+
return element;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
*
|
|
11
|
+
* @param {HTMLElement} element
|
|
12
|
+
* @returns {HTMLElement}
|
|
13
|
+
*/
|
|
14
|
+
function HtmlElementEventsWrapper(element) {
|
|
15
|
+
|
|
16
|
+
if(!element.nd) {
|
|
17
|
+
element.nd = {};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {Object<string,Function>} events
|
|
22
|
+
*/
|
|
23
|
+
element.nd.on = function(events) {
|
|
24
|
+
for(const event in events) {
|
|
25
|
+
const callback = events[event];
|
|
26
|
+
eventWrapper(element, event, callback);
|
|
27
|
+
}
|
|
28
|
+
return element;
|
|
29
|
+
};
|
|
30
|
+
element.nd.on.prevent = function(events) {
|
|
31
|
+
for(const event in events) {
|
|
32
|
+
const callback = events[event];
|
|
33
|
+
eventWrapper(element, event, (event) => {
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
callback && callback(event);
|
|
36
|
+
return element;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return element;
|
|
40
|
+
};
|
|
41
|
+
const events = {
|
|
42
|
+
click: (callback) => eventWrapper(element, 'click', callback),
|
|
43
|
+
focus: (callback) => eventWrapper(element, 'focus', callback),
|
|
44
|
+
blur: (callback) => eventWrapper(element, 'blur', callback),
|
|
45
|
+
input: (callback) => eventWrapper(element, 'input', callback),
|
|
46
|
+
change: (callback) => eventWrapper(element, 'change', callback),
|
|
47
|
+
keyup: (callback) => eventWrapper(element, 'keyup', callback),
|
|
48
|
+
keydown: (callback) => eventWrapper(element, 'keydown', callback),
|
|
49
|
+
beforeInput: (callback) => eventWrapper(element, 'beforeinput', callback),
|
|
50
|
+
mouseOver: (callback) => eventWrapper(element, 'mouseover', callback),
|
|
51
|
+
mouseOut: (callback) => eventWrapper(element, 'mouseout', callback),
|
|
52
|
+
mouseDown: (callback) => eventWrapper(element, 'mousedown', callback),
|
|
53
|
+
mouseUp: (callback) => eventWrapper(element, 'mouseup', callback),
|
|
54
|
+
mouseMove: (callback) => eventWrapper(element, 'mousemove', callback),
|
|
55
|
+
hover: (mouseInCallback, mouseOutCallback) => {
|
|
56
|
+
element.addEventListener('mouseover', mouseInCallback);
|
|
57
|
+
element.addEventListener('mouseout', mouseOutCallback);
|
|
58
|
+
},
|
|
59
|
+
dropped: (callback) => eventWrapper(element, 'drop', callback),
|
|
60
|
+
submit: (callback) => eventWrapper(element, 'submit', callback),
|
|
61
|
+
dragEnd: (callback) => eventWrapper(element, 'dragend', callback),
|
|
62
|
+
dragStart: (callback) => eventWrapper(element, 'dragstart', callback),
|
|
63
|
+
drop: (callback) => eventWrapper(element, 'drop', callback),
|
|
64
|
+
dragOver: (callback) => eventWrapper(element, 'dragover', callback),
|
|
65
|
+
dragEnter: (callback) => eventWrapper(element, 'dragenter', callback),
|
|
66
|
+
dragLeave: (callback) => eventWrapper(element, 'dragleave', callback),
|
|
67
|
+
};
|
|
68
|
+
for(let event in events) {
|
|
69
|
+
element.nd.on[event] = events[event];
|
|
70
|
+
element.nd.on.prevent[event] = function(callback) {
|
|
71
|
+
eventWrapper(element, event.toLowerCase(), (event) => {
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
callback && callback(event);
|
|
74
|
+
});
|
|
75
|
+
return element;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return element;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const DebugManager = {
|
|
83
|
+
enabled: false,
|
|
84
|
+
|
|
85
|
+
enable() {
|
|
86
|
+
this.enabled = true;
|
|
87
|
+
console.log('🔍 NativeDocument Debug Mode enabled');
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
disable() {
|
|
91
|
+
this.enabled = false;
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
log(category, message, data) {
|
|
95
|
+
if (!this.enabled) return;
|
|
96
|
+
console.group(`🔍 [${category}] ${message}`);
|
|
97
|
+
if (data) console.log(data);
|
|
98
|
+
console.trace();
|
|
99
|
+
console.groupEnd();
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
warn(category, message, data) {
|
|
103
|
+
if (!this.enabled) return;
|
|
104
|
+
console.warn(`⚠️ [${category}] ${message}`, data);
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
error(category, message, error) {
|
|
108
|
+
console.error(`❌ [${category}] ${message}`, error);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const MemoryManager = (function() {
|
|
113
|
+
|
|
114
|
+
let $nexObserverId = 0;
|
|
115
|
+
const $observables = new Map();
|
|
116
|
+
let $registry = null;
|
|
117
|
+
try {
|
|
118
|
+
$registry = new FinalizationRegistry((heldValue) => {
|
|
119
|
+
DebugManager.log('MemoryManager', '🧹 Auto-cleanup observable:', heldValue);
|
|
120
|
+
heldValue.listeners.splice(0);
|
|
121
|
+
});
|
|
122
|
+
} catch (e) {
|
|
123
|
+
DebugManager.warn('MemoryManager', 'FinalizationRegistry not supported, observables will not be cleaned automatically');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
/**
|
|
128
|
+
* Register an observable and return an id.
|
|
129
|
+
*
|
|
130
|
+
* @param {ObservableItem} observable
|
|
131
|
+
* @param {Function[]} listeners
|
|
132
|
+
* @returns {number}
|
|
133
|
+
*/
|
|
134
|
+
register(observable, listeners) {
|
|
135
|
+
const id = ++$nexObserverId;
|
|
136
|
+
const heldValue = {
|
|
137
|
+
id: id,
|
|
138
|
+
listeners
|
|
139
|
+
};
|
|
140
|
+
if($registry) {
|
|
141
|
+
$registry.register(observable, heldValue);
|
|
142
|
+
}
|
|
143
|
+
$observables.set(id, new WeakRef(observable));
|
|
144
|
+
return id;
|
|
145
|
+
},
|
|
146
|
+
getObservableById(id) {
|
|
147
|
+
return $observables.get(id)?.deref();
|
|
148
|
+
},
|
|
149
|
+
cleanup() {
|
|
150
|
+
for (const [_, weakObservableRef] of $observables) {
|
|
151
|
+
const observable = weakObservableRef.deref();
|
|
152
|
+
if (observable) {
|
|
153
|
+
observable.cleanup();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
$observables.clear();
|
|
157
|
+
},
|
|
158
|
+
/**
|
|
159
|
+
* Clean observables that are not referenced anymore.
|
|
160
|
+
* @param {number} threshold
|
|
161
|
+
*/
|
|
162
|
+
cleanObservables(threshold) {
|
|
163
|
+
if($observables.size < threshold) return;
|
|
164
|
+
let cleanedCount = 0;
|
|
165
|
+
for (const [id, weakObservableRef] of $observables) {
|
|
166
|
+
if (!weakObservableRef.deref()) {
|
|
167
|
+
$observables.delete(id);
|
|
168
|
+
cleanedCount++;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (cleanedCount > 0) {
|
|
172
|
+
DebugManager.log('Memory Auto Clean', `🧹 Cleaned ${cleanedCount} orphaned observables`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}());
|
|
177
|
+
|
|
178
|
+
class NativeDocumentError extends Error {
|
|
179
|
+
constructor(message, context = {}) {
|
|
180
|
+
super(message);
|
|
181
|
+
this.name = 'NativeDocumentError';
|
|
182
|
+
this.context = context;
|
|
183
|
+
this.timestamp = new Date().toISOString();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
*
|
|
189
|
+
* @param {ObservableItem} $observable
|
|
190
|
+
* @param {Function} $checker
|
|
191
|
+
* @class ObservableChecker
|
|
192
|
+
*/
|
|
193
|
+
function ObservableChecker($observable, $checker) {
|
|
194
|
+
this.observable = $observable;
|
|
195
|
+
this.checker = $checker;
|
|
196
|
+
|
|
197
|
+
this.subscribe = function(callback) {
|
|
198
|
+
return $observable.subscribe((value) => {
|
|
199
|
+
callback && callback($checker(value));
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
this.val = function() {
|
|
204
|
+
return $checker && $checker($observable.val());
|
|
205
|
+
};
|
|
206
|
+
this.check = function(callback) {
|
|
207
|
+
return $observable.check(() => callback(this.val()));
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
this.set = function(value) {
|
|
211
|
+
return $observable.set(value);
|
|
212
|
+
};
|
|
213
|
+
this.trigger = function() {
|
|
214
|
+
return $observable.trigger();
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
this.cleanup = function() {
|
|
218
|
+
return $observable.cleanup();
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
*
|
|
224
|
+
* @param {*} value
|
|
225
|
+
* @class ObservableItem
|
|
226
|
+
*/
|
|
227
|
+
function ObservableItem(value) {
|
|
228
|
+
if (value === undefined) {
|
|
229
|
+
throw new NativeDocumentError('ObservableItem requires an initial value');
|
|
230
|
+
}
|
|
231
|
+
if(value instanceof ObservableItem) {
|
|
232
|
+
throw new NativeDocumentError('ObservableItem cannot be an Observable');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const $initialValue = (typeof value === 'object') ? JSON.parse(JSON.stringify(value)) : value;
|
|
236
|
+
|
|
237
|
+
let $previousValue = value;
|
|
238
|
+
let $currentValue = value;
|
|
239
|
+
let $isCleanedUp = false;
|
|
240
|
+
|
|
241
|
+
const $listeners = [];
|
|
242
|
+
|
|
243
|
+
const $memoryId = MemoryManager.register(this, $listeners);
|
|
244
|
+
|
|
245
|
+
this.trigger = () => {
|
|
246
|
+
$listeners.forEach(listener => {
|
|
247
|
+
try {
|
|
248
|
+
listener($currentValue, $previousValue);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
DebugManager.error('Listener Undefined', 'Error in observable listener:', error);
|
|
251
|
+
this.unsubscribe(listener);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
this.originalValue = () => $initialValue;
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* @param {*} data
|
|
261
|
+
*/
|
|
262
|
+
this.set = (data) => {
|
|
263
|
+
const newValue = (typeof data === 'function') ? data($currentValue) : data;
|
|
264
|
+
if($currentValue === newValue) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
$previousValue = $currentValue;
|
|
268
|
+
$currentValue = newValue;
|
|
269
|
+
this.trigger();
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
this.val = () => $currentValue;
|
|
273
|
+
|
|
274
|
+
this.cleanup = function() {
|
|
275
|
+
$listeners.splice(0);
|
|
276
|
+
$isCleanedUp = true;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
*
|
|
281
|
+
* @param {Function} callback
|
|
282
|
+
* @returns {(function(): void)}
|
|
283
|
+
*/
|
|
284
|
+
this.subscribe = (callback) => {
|
|
285
|
+
if ($isCleanedUp) {
|
|
286
|
+
DebugManager.warn('Observable subscription', '⚠️ Attempted to subscribe to a cleaned up observable.');
|
|
287
|
+
return () => {};
|
|
288
|
+
}
|
|
289
|
+
if (typeof callback !== 'function') {
|
|
290
|
+
throw new NativeDocumentError('Callback must be a function');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
$listeners.push(callback);
|
|
294
|
+
return () => this.unsubscribe(callback);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Unsubscribe from an observable.
|
|
299
|
+
* @param {Function} callback
|
|
300
|
+
*/
|
|
301
|
+
this.unsubscribe = (callback) => {
|
|
302
|
+
const index = $listeners.indexOf(callback);
|
|
303
|
+
if (index > -1) {
|
|
304
|
+
$listeners.splice(index, 1);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Create an Observable checker instance
|
|
310
|
+
* @param callback
|
|
311
|
+
* @returns {ObservableChecker}
|
|
312
|
+
*/
|
|
313
|
+
this.check = function(callback) {
|
|
314
|
+
return new ObservableChecker(this, callback)
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const $object = this;
|
|
318
|
+
Object.defineProperty($object, '$value', {
|
|
319
|
+
get() {
|
|
320
|
+
return $object.val();
|
|
321
|
+
},
|
|
322
|
+
set(value) {
|
|
323
|
+
$object.set(value);
|
|
324
|
+
return $object;
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
this.toString = function() {
|
|
329
|
+
return '{{#ObItem::(' +$memoryId+ ')}}';
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const Validator = {
|
|
335
|
+
isObservable(value) {
|
|
336
|
+
return value instanceof ObservableItem || value instanceof ObservableChecker;
|
|
337
|
+
},
|
|
338
|
+
isProxy(value) {
|
|
339
|
+
return value?.__isProxy__
|
|
340
|
+
},
|
|
341
|
+
isObservableChecker(value) {
|
|
342
|
+
return value instanceof ObservableChecker;
|
|
343
|
+
},
|
|
344
|
+
isArray(value) {
|
|
345
|
+
return Array.isArray(value);
|
|
346
|
+
},
|
|
347
|
+
isString(value) {
|
|
348
|
+
return typeof value === 'string';
|
|
349
|
+
},
|
|
350
|
+
isNumber(value) {
|
|
351
|
+
return typeof value === 'number';
|
|
352
|
+
},
|
|
353
|
+
isBoolean(value) {
|
|
354
|
+
return typeof value === 'boolean';
|
|
355
|
+
},
|
|
356
|
+
isFunction(value) {
|
|
357
|
+
return typeof value === 'function';
|
|
358
|
+
},
|
|
359
|
+
isObject(value) {
|
|
360
|
+
return typeof value === 'object';
|
|
361
|
+
},
|
|
362
|
+
isJson(value) {
|
|
363
|
+
return typeof value === 'object' && value !== null && value.constructor.name === 'Object' && !Array.isArray(value);
|
|
364
|
+
},
|
|
365
|
+
isElement(value) {
|
|
366
|
+
return value instanceof HTMLElement || value instanceof DocumentFragment || value instanceof Text;
|
|
367
|
+
},
|
|
368
|
+
isFragment(value) {
|
|
369
|
+
return value instanceof DocumentFragment;
|
|
370
|
+
},
|
|
371
|
+
isStringOrObservable(value) {
|
|
372
|
+
return this.isString(value) || this.isObservable(value);
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
isValidChild(child) {
|
|
376
|
+
return child === null ||
|
|
377
|
+
this.isElement(child) ||
|
|
378
|
+
this.isObservable(child) ||
|
|
379
|
+
['string', 'number', 'boolean'].includes(typeof child);
|
|
380
|
+
},
|
|
381
|
+
isValidChildren(children) {
|
|
382
|
+
if (!Array.isArray(children)) {
|
|
383
|
+
children = [children];
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const invalid = children.filter(child => !this.isValidChild(child));
|
|
387
|
+
return invalid.length === 0;
|
|
388
|
+
},
|
|
389
|
+
validateChildren(children) {
|
|
390
|
+
if (!Array.isArray(children)) {
|
|
391
|
+
children = [children];
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const invalid = children.filter(child => !this.isValidChild(child));
|
|
395
|
+
if (invalid.length > 0) {
|
|
396
|
+
throw new NativeDocumentError(`Invalid children detected: ${invalid.map(i => typeof i).join(', ')}`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return children;
|
|
400
|
+
},
|
|
401
|
+
/**
|
|
402
|
+
* Check if the data contains observables.
|
|
403
|
+
* @param {Array|Object} data
|
|
404
|
+
* @returns {boolean}
|
|
405
|
+
*/
|
|
406
|
+
containsObservables(data) {
|
|
407
|
+
if(!data) {
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
return Validator.isObject(data)
|
|
411
|
+
&& Object.values(data).some(value => Validator.isObservable(value));
|
|
412
|
+
},
|
|
413
|
+
/**
|
|
414
|
+
* Check if the data contains an observable reference.
|
|
415
|
+
* @param {string} data
|
|
416
|
+
* @returns {boolean}
|
|
417
|
+
*/
|
|
418
|
+
containsObservableReference(data) {
|
|
419
|
+
if(!data || typeof data !== 'string') {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
return /\{\{#ObItem::\([0-9]+\)\}\}/.test(data);
|
|
423
|
+
},
|
|
424
|
+
validateAttributes(attributes) {
|
|
425
|
+
if (!attributes || typeof attributes !== 'object') {
|
|
426
|
+
return attributes;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const reserved = [];
|
|
430
|
+
const foundReserved = Object.keys(attributes).filter(key => reserved.includes(key));
|
|
431
|
+
|
|
432
|
+
if (foundReserved.length > 0) {
|
|
433
|
+
DebugManager.warn('Validator', `Reserved attributes found: ${foundReserved.join(', ')}`);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return attributes;
|
|
437
|
+
},
|
|
438
|
+
|
|
439
|
+
validateEventCallback(callback) {
|
|
440
|
+
if (typeof callback !== 'function') {
|
|
441
|
+
throw new NativeDocumentError('Event callback must be a function');
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
const BOOLEAN_ATTRIBUTES = ['checked', 'selected', 'disabled', 'readonly', 'required', 'autofocus', 'multiple', 'autocomplete', 'hidden', 'contenteditable', 'spellcheck', 'translate', 'draggable', 'async', 'defer', 'autoplay', 'controls', 'loop', 'muted', 'download', 'reversed', 'open', 'default', 'formnovalidate', 'novalidate', 'scoped', 'itemscope', 'allowfullscreen', 'allowpaymentrequest', 'playsinline'];
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
*
|
|
450
|
+
* @param {Function} fn
|
|
451
|
+
* @param {number} delay
|
|
452
|
+
* @param {{leading?:Boolean, trailing?:Boolean, debounce?:Boolean}}options
|
|
453
|
+
* @returns {(function(...[*]): void)|*}
|
|
454
|
+
*/
|
|
455
|
+
const throttle = function(fn, delay, options = {}) {
|
|
456
|
+
let timer = null;
|
|
457
|
+
let lastExecTime = 0;
|
|
458
|
+
const { leading = true, trailing = true, debounce = false } = options;
|
|
459
|
+
|
|
460
|
+
return function(...args) {
|
|
461
|
+
const now = Date.now();
|
|
462
|
+
if (debounce) {
|
|
463
|
+
// debounce mode: reset the timer for each call
|
|
464
|
+
clearTimeout(timer);
|
|
465
|
+
timer = setTimeout(() => fn.apply(this, args), delay);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (leading && now - lastExecTime >= delay) {
|
|
469
|
+
fn.apply(this, args);
|
|
470
|
+
lastExecTime = now;
|
|
471
|
+
}
|
|
472
|
+
if (trailing && !timer) {
|
|
473
|
+
timer = setTimeout(() => {
|
|
474
|
+
fn.apply(this, args);
|
|
475
|
+
lastExecTime = Date.now();
|
|
476
|
+
timer = null;
|
|
477
|
+
}, delay - (now - lastExecTime));
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
const trim = function(str, char) {
|
|
483
|
+
return str.replace(new RegExp(`^[${char}]+|[${char}]+$`, 'g'), '');
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
*
|
|
488
|
+
* @param {*} value
|
|
489
|
+
* @returns {ObservableItem}
|
|
490
|
+
* @constructor
|
|
491
|
+
*/
|
|
492
|
+
function Observable(value) {
|
|
493
|
+
return new ObservableItem(value);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
Observable.computed = function(callback, dependencies = []) {
|
|
497
|
+
const initialValue = callback();
|
|
498
|
+
const observable = new ObservableItem(initialValue);
|
|
499
|
+
|
|
500
|
+
const updatedValue = () => observable.set(callback());
|
|
501
|
+
|
|
502
|
+
dependencies.forEach(dependency => dependency.subscribe(updatedValue));
|
|
503
|
+
|
|
504
|
+
return observable;
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
*
|
|
509
|
+
* @param id
|
|
510
|
+
* @returns {ObservableItem|null}
|
|
511
|
+
*/
|
|
512
|
+
Observable.getById = function(id) {
|
|
513
|
+
const item = MemoryManager.getObservableById(parseInt(id));
|
|
514
|
+
if(!item) {
|
|
515
|
+
throw new NativeDocumentError('Observable.getById : No observable found with id ' + id);
|
|
516
|
+
}
|
|
517
|
+
return item;
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
*
|
|
523
|
+
* @param {ObservableItem} observable
|
|
524
|
+
*/
|
|
525
|
+
Observable.cleanup = function(observable) {
|
|
526
|
+
observable.cleanup();
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Get the value of an observable or an object of observables.
|
|
531
|
+
* @param {ObservableItem|Object<ObservableItem>} object
|
|
532
|
+
* @returns {{}|*|null}
|
|
533
|
+
*/
|
|
534
|
+
Observable.value = function(data) {
|
|
535
|
+
if(Validator.isObservable(data)) {
|
|
536
|
+
return data.val();
|
|
537
|
+
}
|
|
538
|
+
if(Validator.isProxy(data)) {
|
|
539
|
+
return data.$val();
|
|
540
|
+
}
|
|
541
|
+
if(Validator.isArray(data)) {
|
|
542
|
+
const result = [];
|
|
543
|
+
data.forEach(item => {
|
|
544
|
+
result.push(Observable.value(item));
|
|
545
|
+
});
|
|
546
|
+
return result;
|
|
547
|
+
}
|
|
548
|
+
return data;
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
*
|
|
553
|
+
* @param {Object} value
|
|
554
|
+
* @returns {Proxy}
|
|
555
|
+
*/
|
|
556
|
+
Observable.init = function(value) {
|
|
557
|
+
const data = {};
|
|
558
|
+
for(const key in value) {
|
|
559
|
+
const itemValue = value[key];
|
|
560
|
+
if(Validator.isJson(itemValue)) {
|
|
561
|
+
data[key] = Observable.init(itemValue);
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
else if(Validator.isArray(itemValue)) {
|
|
565
|
+
data[key] = Observable.array(itemValue);
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
data[key] = Observable(itemValue);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const $val = function() {
|
|
572
|
+
const result = {};
|
|
573
|
+
for(const key in data) {
|
|
574
|
+
const dataItem = data[key];
|
|
575
|
+
if(Validator.isObservable(dataItem)) {
|
|
576
|
+
result[key] = dataItem.val();
|
|
577
|
+
} else if(Validator.isProxy(dataItem)) {
|
|
578
|
+
result[key] = dataItem.$val();
|
|
579
|
+
} else {
|
|
580
|
+
result[key] = dataItem;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return result;
|
|
584
|
+
};
|
|
585
|
+
const $clone = function() {
|
|
586
|
+
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
return new Proxy(data, {
|
|
590
|
+
get(target, property) {
|
|
591
|
+
if(property === '__isProxy__') {
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
if(property === '$val') {
|
|
595
|
+
return $val;
|
|
596
|
+
}
|
|
597
|
+
if(property === '$clone') {
|
|
598
|
+
return $clone;
|
|
599
|
+
}
|
|
600
|
+
if(target[property] !== undefined) {
|
|
601
|
+
return target[property];
|
|
602
|
+
}
|
|
603
|
+
return undefined;
|
|
604
|
+
},
|
|
605
|
+
set(target, prop, newValue) {
|
|
606
|
+
if(target[prop] !== undefined) {
|
|
607
|
+
target[prop].set(newValue);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
})
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
Observable.object = Observable.init;
|
|
614
|
+
Observable.json = Observable.init;
|
|
615
|
+
Observable.update = function($target, data) {
|
|
616
|
+
for(const key in data) {
|
|
617
|
+
const targetItem = $target[key];
|
|
618
|
+
const newValue = data[key];
|
|
619
|
+
|
|
620
|
+
if(Validator.isObservable(targetItem)) {
|
|
621
|
+
if(Validator.isArray(newValue)) {
|
|
622
|
+
Observable.update(targetItem, newValue);
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
targetItem.set(newValue);
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
if(Validator.isProxy(targetItem)) {
|
|
629
|
+
Observable.update(targetItem, newValue);
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
$target[key] = newValue;
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
/**
|
|
636
|
+
*
|
|
637
|
+
* @param {Array} target
|
|
638
|
+
* @returns {ObservableItem}
|
|
639
|
+
*/
|
|
640
|
+
Observable.array = function(target) {
|
|
641
|
+
if(!Array.isArray(target)) {
|
|
642
|
+
throw new NativeDocumentError('Observable.array : target must be an array');
|
|
643
|
+
}
|
|
644
|
+
const observer = Observable(target);
|
|
645
|
+
|
|
646
|
+
const methods = ['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice'];
|
|
647
|
+
|
|
648
|
+
methods.forEach((method) => {
|
|
649
|
+
observer[method] = function(...values) {
|
|
650
|
+
const target = observer.val();
|
|
651
|
+
const result = target[method].apply(target, arguments);
|
|
652
|
+
observer.trigger();
|
|
653
|
+
return result;
|
|
654
|
+
};
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
const overrideMethods = ['map', 'filter', 'reduce', 'some', 'every', 'find'];
|
|
658
|
+
overrideMethods.forEach((method) => {
|
|
659
|
+
observer[method] = function(callback) {
|
|
660
|
+
return observer.val()[method](callback);
|
|
661
|
+
};
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
return observer;
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Enable auto cleanup of observables.
|
|
669
|
+
* @param {Boolean} enable
|
|
670
|
+
* @param {{interval:Boolean, threshold:number}} options
|
|
671
|
+
*/
|
|
672
|
+
Observable.autoCleanup = function(enable = false, options = {}) {
|
|
673
|
+
if(!enable) {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
const { interval = 60000, threshold = 100 } = options;
|
|
677
|
+
|
|
678
|
+
window.addEventListener('beforeunload', () => {
|
|
679
|
+
MemoryManager.cleanup();
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
setInterval(() => MemoryManager.cleanObservables(threshold), interval);
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
*
|
|
687
|
+
* @param {HTMLElement} element
|
|
688
|
+
* @param {string} className
|
|
689
|
+
* @param {string} value
|
|
690
|
+
*/
|
|
691
|
+
const toggleClassItem = function(element, className, value) {
|
|
692
|
+
if(value) {
|
|
693
|
+
element.classList.add(className);
|
|
694
|
+
} else {
|
|
695
|
+
element.classList.remove(className);
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
*
|
|
701
|
+
* @param {HTMLElement} element
|
|
702
|
+
* @param {Object} data
|
|
703
|
+
*/
|
|
704
|
+
function bindClassAttribute(element, data) {
|
|
705
|
+
for(let className in data) {
|
|
706
|
+
const value = data[className];
|
|
707
|
+
if(Validator.isObservable(value)) {
|
|
708
|
+
toggleClassItem(element, className, value.val());
|
|
709
|
+
value.subscribe(newValue => toggleClassItem(element, className, newValue));
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
toggleClassItem(element, className, value);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
*
|
|
718
|
+
* @param {HTMLElement} element
|
|
719
|
+
* @param {Object} data
|
|
720
|
+
*/
|
|
721
|
+
function bindStyleAttribute(element, data) {
|
|
722
|
+
for(let styleName in data) {
|
|
723
|
+
const value = data[styleName];
|
|
724
|
+
if(Validator.isObservable(value)) {
|
|
725
|
+
element.style[styleName] = value.val();
|
|
726
|
+
value.subscribe(newValue => {
|
|
727
|
+
element.style[styleName] = newValue;
|
|
728
|
+
});
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
element.style[styleName] = value;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
*
|
|
737
|
+
* @param {HTMLElement} element
|
|
738
|
+
* @param {string} attributeName
|
|
739
|
+
* @param {boolean|number|Observable} value
|
|
740
|
+
*/
|
|
741
|
+
function bindBooleanAttribute(element, attributeName, value) {
|
|
742
|
+
const defaultValue = Validator.isObservable(value) ? value.val() : value;
|
|
743
|
+
if(Validator.isBoolean(defaultValue)) {
|
|
744
|
+
element[attributeName] = defaultValue;
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
element[attributeName] = defaultValue === element.value;
|
|
748
|
+
}
|
|
749
|
+
if(Validator.isObservable(value)) {
|
|
750
|
+
if(['checked'].includes(attributeName)) {
|
|
751
|
+
element.addEventListener('input', () => {
|
|
752
|
+
if(Validator.isBoolean(defaultValue)) {
|
|
753
|
+
value.set(element[attributeName]);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
value.set(element.value);
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
value.subscribe(newValue => {
|
|
760
|
+
if(Validator.isBoolean(newValue)) {
|
|
761
|
+
element[attributeName] = newValue;
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
element[attributeName] = newValue === element.value;
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
*
|
|
772
|
+
* @param {HTMLElement} element
|
|
773
|
+
* @param {string} attributeName
|
|
774
|
+
* @param {Observable} value
|
|
775
|
+
*/
|
|
776
|
+
function bindAttributeWithObservable(element, attributeName, value) {
|
|
777
|
+
const applyValue = (newValue) => {
|
|
778
|
+
if(attributeName === 'value') {
|
|
779
|
+
element.value = newValue;
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
element.setAttribute(attributeName, newValue);
|
|
783
|
+
};
|
|
784
|
+
value.subscribe(applyValue);
|
|
785
|
+
applyValue(value.val());
|
|
786
|
+
|
|
787
|
+
if(attributeName === 'value') {
|
|
788
|
+
element.addEventListener('input', () => value.set(element.value));
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
*
|
|
794
|
+
* @param {HTMLElement} element
|
|
795
|
+
* @param {Object} attributes
|
|
796
|
+
*/
|
|
797
|
+
function AttributesWrapper(element, attributes) {
|
|
798
|
+
|
|
799
|
+
Validator.validateAttributes(attributes);
|
|
800
|
+
|
|
801
|
+
if(!Validator.isObject(attributes)) {
|
|
802
|
+
throw new NativeDocumentError('Attributes must be an object');
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
for(let key in attributes) {
|
|
806
|
+
const attributeName = key.toLowerCase();
|
|
807
|
+
let value = attributes[attributeName];
|
|
808
|
+
if(Validator.isString(value) && Validator.isFunction(value.resolveObservableTemplate)) {
|
|
809
|
+
value = value.resolveObservableTemplate();
|
|
810
|
+
if(Validator.isArray(value)) {
|
|
811
|
+
const observables = value.filter(item => Validator.isObservable(item));
|
|
812
|
+
value = Observable.computed(() => {
|
|
813
|
+
return value.map(item => Validator.isObservable(item) ? item.val() : item).join(' ') || ' ';
|
|
814
|
+
}, observables);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if(BOOLEAN_ATTRIBUTES.includes(attributeName)) {
|
|
818
|
+
bindBooleanAttribute(element, attributeName, value);
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
if(Validator.isObservable(value)) {
|
|
822
|
+
bindAttributeWithObservable(element, attributeName, value);
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
if(attributeName === 'class' && Validator.isJson(value)) {
|
|
826
|
+
bindClassAttribute(element, value);
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
if(attributeName === 'style' && Validator.isJson(value)) {
|
|
830
|
+
bindStyleAttribute(element, value);
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
element.setAttribute(attributeName, value);
|
|
834
|
+
}
|
|
835
|
+
return element;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const DocumentObserver = {
|
|
839
|
+
elements: new Map(),
|
|
840
|
+
observer: null,
|
|
841
|
+
checkMutation: throttle(function() {
|
|
842
|
+
for(const [element, data] of DocumentObserver.elements.entries()) {
|
|
843
|
+
const isCurrentlyInDom = document.body.contains(element);
|
|
844
|
+
if(isCurrentlyInDom && !data.inDom) {
|
|
845
|
+
data.inDom = true;
|
|
846
|
+
data.mounted.forEach(callback => callback(element));
|
|
847
|
+
} else if(!isCurrentlyInDom && data.inDom) {
|
|
848
|
+
data.inDom = false;
|
|
849
|
+
data.unmounted.forEach(callback => callback(element));
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}, 10, { debounce: true }),
|
|
853
|
+
/**
|
|
854
|
+
*
|
|
855
|
+
* @param {HTMLElement} element
|
|
856
|
+
* @returns {{watch: (function(): Map<any, any>), disconnect: (function(): boolean), mounted: (function(*): Set<any>), unmounted: (function(*): Set<any>)}}
|
|
857
|
+
*/
|
|
858
|
+
watch: function(element) {
|
|
859
|
+
let data = {};
|
|
860
|
+
if(DocumentObserver.elements.has(element)) {
|
|
861
|
+
data = DocumentObserver.elements.get(element);
|
|
862
|
+
} else {
|
|
863
|
+
const inDom = document.body.contains(element);
|
|
864
|
+
data = {
|
|
865
|
+
inDom,
|
|
866
|
+
mounted: new Set(),
|
|
867
|
+
unmounted: new Set(),
|
|
868
|
+
};
|
|
869
|
+
DocumentObserver.elements.set(element, data);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
return {
|
|
873
|
+
watch: () => DocumentObserver.elements.set(element, data),
|
|
874
|
+
disconnect: () => DocumentObserver.elements.delete(element),
|
|
875
|
+
mounted: (callback) => data.mounted.add(callback),
|
|
876
|
+
unmounted: (callback) => data.unmounted.add(callback)
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
DocumentObserver.observer = new MutationObserver(DocumentObserver.checkMutation);
|
|
882
|
+
DocumentObserver.observer.observe(document.body, {
|
|
883
|
+
childList: true,
|
|
884
|
+
subtree: true,
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
const getChildAsNode = (child) => {
|
|
888
|
+
if(Validator.isFunction(child)) {
|
|
889
|
+
return getChildAsNode(child());
|
|
890
|
+
}
|
|
891
|
+
if(Validator.isElement(child)) {
|
|
892
|
+
return child;
|
|
893
|
+
}
|
|
894
|
+
return createTextNode(String(child))
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
function Anchor(name) {
|
|
898
|
+
const element = document.createDocumentFragment();
|
|
899
|
+
|
|
900
|
+
const anchorStart = document.createComment('Anchor Start : '+name);
|
|
901
|
+
const anchorEnd = document.createComment('/ Anchor End '+name);
|
|
902
|
+
|
|
903
|
+
element.appendChild(anchorStart);
|
|
904
|
+
element.appendChild(anchorEnd);
|
|
905
|
+
|
|
906
|
+
element.nativeInsertBefore = element.insertBefore;
|
|
907
|
+
element.nativeAppendChild = element.appendChild;
|
|
908
|
+
|
|
909
|
+
const insertBefore = function(parent, child, target) {
|
|
910
|
+
if(parent === element) {
|
|
911
|
+
parent.nativeInsertBefore(getChildAsNode(child), target);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
parent.insertBefore(getChildAsNode(child), target);
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
element.appendChild = function(child, before = null) {
|
|
918
|
+
const parent = anchorEnd.parentNode;
|
|
919
|
+
if(!parent) {
|
|
920
|
+
DebugManager.error('Anchor', 'Anchor : parent not found', child);
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
before = before ?? anchorEnd;
|
|
924
|
+
if(Validator.isArray(child)) {
|
|
925
|
+
child.forEach((element) => {
|
|
926
|
+
insertBefore(parent, element, before);
|
|
927
|
+
});
|
|
928
|
+
return element;
|
|
929
|
+
}
|
|
930
|
+
insertBefore(parent, child, before);
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
element.remove = function(trueRemove) {
|
|
934
|
+
if(anchorEnd.parentNode === element) {
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
let itemToRemove = anchorStart.nextSibling, tempItem;
|
|
938
|
+
while(itemToRemove !== anchorEnd) {
|
|
939
|
+
tempItem = itemToRemove.nextSibling;
|
|
940
|
+
trueRemove ? itemToRemove.remove() : element.nativeAppendChild(itemToRemove);
|
|
941
|
+
itemToRemove = tempItem;
|
|
942
|
+
}
|
|
943
|
+
if(trueRemove) {
|
|
944
|
+
anchorEnd.remove();
|
|
945
|
+
anchorStart.remove();
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
element.insertBefore = function(child, anchor = null) {
|
|
950
|
+
element.appendChild(child, anchor);
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
element.clear = function() {
|
|
954
|
+
element.remove();
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
element.endElement = function() {
|
|
958
|
+
return anchorEnd;
|
|
959
|
+
};
|
|
960
|
+
element.startElement = function() {
|
|
961
|
+
return anchorStart;
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
return element;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const pluginsManager = (function() {
|
|
968
|
+
|
|
969
|
+
const $plugins = [];
|
|
970
|
+
|
|
971
|
+
return {
|
|
972
|
+
list : () => $plugins,
|
|
973
|
+
add : (plugin) => $plugins.push(plugin)
|
|
974
|
+
};
|
|
975
|
+
}());
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
*
|
|
979
|
+
* @param {HTMLElement|DocumentFragment} parent
|
|
980
|
+
* @param {ObservableItem} observable
|
|
981
|
+
* @returns {Text}
|
|
982
|
+
*/
|
|
983
|
+
const createObservableNode = function(parent, observable) {
|
|
984
|
+
const text = document.createTextNode('');
|
|
985
|
+
observable.subscribe(value => text.textContent = String(value));
|
|
986
|
+
text.textContent = observable.val();
|
|
987
|
+
parent && parent.appendChild(text);
|
|
988
|
+
return text;
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
*
|
|
993
|
+
* @param {HTMLElement|DocumentFragment} parent
|
|
994
|
+
* @param {*} value
|
|
995
|
+
* @returns {Text}
|
|
996
|
+
*/
|
|
997
|
+
const createStaticTextNode = function(parent, value) {
|
|
998
|
+
const text = document.createTextNode('');
|
|
999
|
+
text.textContent = String(value);
|
|
1000
|
+
parent && parent.appendChild(text);
|
|
1001
|
+
return text;
|
|
1002
|
+
};
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
*
|
|
1006
|
+
* @param {HTMLElement} element
|
|
1007
|
+
*/
|
|
1008
|
+
const addUtilsMethods = function(element) {
|
|
1009
|
+
element.nd.wrap = (callback) => {
|
|
1010
|
+
if(!Validator.isFunction(callback)) {
|
|
1011
|
+
throw new NativeDocumentError('Callback must be a function');
|
|
1012
|
+
}
|
|
1013
|
+
callback && callback(element);
|
|
1014
|
+
return element;
|
|
1015
|
+
};
|
|
1016
|
+
element.nd.ref = (target, name) => {
|
|
1017
|
+
target[name] = element;
|
|
1018
|
+
return element;
|
|
1019
|
+
};
|
|
1020
|
+
|
|
1021
|
+
let $observer = null;
|
|
1022
|
+
|
|
1023
|
+
element.nd.appendChild = function(child) {
|
|
1024
|
+
if(Validator.isArray(child)) {
|
|
1025
|
+
ElementCreator.processChildren(child, element);
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
if(Validator.isFunction(child)) {
|
|
1029
|
+
child = child();
|
|
1030
|
+
ElementCreator.processChildren(child(), element);
|
|
1031
|
+
}
|
|
1032
|
+
if(Validator.isElement(child)) {
|
|
1033
|
+
ElementCreator.processChildren(child, element);
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
element.nd.lifecycle = function(states) {
|
|
1038
|
+
$observer = $observer || DocumentObserver.watch(element);
|
|
1039
|
+
|
|
1040
|
+
states.mounted && $observer.mounted(states.mounted);
|
|
1041
|
+
states.unmounted && $observer.unmounted(states.unmounted);
|
|
1042
|
+
return element;
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1045
|
+
element.nd.mounted = (callback) => {
|
|
1046
|
+
$observer = $observer || DocumentObserver.watch(element);
|
|
1047
|
+
$observer.mounted(callback);
|
|
1048
|
+
return element;
|
|
1049
|
+
};
|
|
1050
|
+
element.nd.unmounted = (callback) => {
|
|
1051
|
+
$observer = $observer || DocumentObserver.watch(element);
|
|
1052
|
+
$observer.unmounted(callback);
|
|
1053
|
+
return element;
|
|
1054
|
+
};
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
*
|
|
1059
|
+
* @param {*} value
|
|
1060
|
+
* @returns {Text}
|
|
1061
|
+
*/
|
|
1062
|
+
const createTextNode = function(value) {
|
|
1063
|
+
return (Validator.isObservable(value))
|
|
1064
|
+
? createObservableNode(null, value)
|
|
1065
|
+
: createStaticTextNode(null, value);
|
|
1066
|
+
};
|
|
1067
|
+
|
|
1068
|
+
const ElementCreator = {
|
|
1069
|
+
/**
|
|
1070
|
+
*
|
|
1071
|
+
* @param {string} name
|
|
1072
|
+
* @returns {HTMLElement|DocumentFragment}
|
|
1073
|
+
*/
|
|
1074
|
+
createElement(name) {
|
|
1075
|
+
return name ? document.createElement(name) : new Anchor('Fragment');
|
|
1076
|
+
},
|
|
1077
|
+
/**
|
|
1078
|
+
*
|
|
1079
|
+
* @param {*} children
|
|
1080
|
+
* @param {HTMLElement|DocumentFragment} parent
|
|
1081
|
+
*/
|
|
1082
|
+
processChildren(children, parent) {
|
|
1083
|
+
if(children === null) return;
|
|
1084
|
+
const childrenArray = Array.isArray(children) ? children : [children];
|
|
1085
|
+
childrenArray.forEach(child => {
|
|
1086
|
+
if (child === null) return;
|
|
1087
|
+
if(Validator.isString(child) && Validator.isFunction(child.resolveObservableTemplate)) {
|
|
1088
|
+
child = child.resolveObservableTemplate();
|
|
1089
|
+
}
|
|
1090
|
+
if(Validator.isFunction(child)) {
|
|
1091
|
+
this.processChildren(child(), parent);
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
if(Validator.isArray(child)) {
|
|
1095
|
+
this.processChildren(child, parent);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
if (Validator.isElement(child)) {
|
|
1099
|
+
parent.appendChild(child);
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
if (Validator.isObservable(child)) {
|
|
1103
|
+
createObservableNode(parent, child);
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
if (child) {
|
|
1107
|
+
createStaticTextNode(parent, child);
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
1110
|
+
},
|
|
1111
|
+
/**
|
|
1112
|
+
*
|
|
1113
|
+
* @param {HTMLElement} element
|
|
1114
|
+
* @param {Object} attributes
|
|
1115
|
+
*/
|
|
1116
|
+
processAttributes(element, attributes) {
|
|
1117
|
+
if(Validator.isFragment(element)) return;
|
|
1118
|
+
if (attributes) {
|
|
1119
|
+
AttributesWrapper(element, attributes);
|
|
1120
|
+
}
|
|
1121
|
+
},
|
|
1122
|
+
/**
|
|
1123
|
+
*
|
|
1124
|
+
* @param {HTMLElement} element
|
|
1125
|
+
* @param {Object} attributes
|
|
1126
|
+
* @param {?Function} customWrapper
|
|
1127
|
+
* @returns {HTMLElement|DocumentFragment}
|
|
1128
|
+
*/
|
|
1129
|
+
setup(element, attributes, customWrapper) {
|
|
1130
|
+
element.nd = {};
|
|
1131
|
+
HtmlElementEventsWrapper(element);
|
|
1132
|
+
const item = (typeof customWrapper === 'function') ? customWrapper(element) : element;
|
|
1133
|
+
addUtilsMethods(item);
|
|
1134
|
+
|
|
1135
|
+
pluginsManager.list().forEach(plugin => {
|
|
1136
|
+
plugin?.element?.setup && plugin.element.setup(item, attributes);
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
return item;
|
|
1140
|
+
}
|
|
1141
|
+
};
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
*
|
|
1145
|
+
* @param {string} name
|
|
1146
|
+
* @param {?Function} customWrapper
|
|
1147
|
+
* @returns {Function}
|
|
1148
|
+
*/
|
|
1149
|
+
function HtmlElementWrapper(name, customWrapper) {
|
|
1150
|
+
const $tagName = name.toLowerCase().trim();
|
|
1151
|
+
|
|
1152
|
+
const builder = function(attributes, children = null) {
|
|
1153
|
+
try {
|
|
1154
|
+
if(Validator.isValidChildren(attributes)) {
|
|
1155
|
+
const tempChildren = children;
|
|
1156
|
+
children = attributes;
|
|
1157
|
+
attributes = tempChildren;
|
|
1158
|
+
}
|
|
1159
|
+
const element = ElementCreator.createElement($tagName);
|
|
1160
|
+
|
|
1161
|
+
ElementCreator.processAttributes(element, attributes);
|
|
1162
|
+
ElementCreator.processChildren(children, element);
|
|
1163
|
+
|
|
1164
|
+
return ElementCreator.setup(element, attributes, customWrapper);
|
|
1165
|
+
} catch (error) {
|
|
1166
|
+
DebugManager.error('ElementCreation', `Error creating ${$tagName}`, error);
|
|
1167
|
+
}
|
|
1168
|
+
};
|
|
1169
|
+
|
|
1170
|
+
builder.hold = (children, attributes) => (() => builder(children, attributes));
|
|
1171
|
+
|
|
1172
|
+
return builder;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
class ArgTypesError extends Error {
|
|
1176
|
+
constructor(message, errors) {
|
|
1177
|
+
super(`${message}\n\n${errors.join("\n")}\n\n`);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
*
|
|
1183
|
+
* @type {{string: (function(*): {name: *, type: string, validate: function(*): boolean}),
|
|
1184
|
+
* number: (function(*): {name: *, type: string, validate: function(*): boolean}),
|
|
1185
|
+
* boolean: (function(*): {name: *, type: string, validate: function(*): boolean}),
|
|
1186
|
+
* observable: (function(*): {name: *, type: string, validate: function(*): boolean}),
|
|
1187
|
+
* element: (function(*): {name: *, type: string, validate: function(*): *}),
|
|
1188
|
+
* function: (function(*): {name: *, type: string, validate: function(*): boolean}),
|
|
1189
|
+
* object: (function(*): {name: *, type: string, validate: function(*): boolean}),
|
|
1190
|
+
* objectNotNull: (function(*): {name: *, type: string, validate: function(*): *}),
|
|
1191
|
+
* children: (function(*): {name: *, type: string, validate: function(*): *}),
|
|
1192
|
+
* attributes: (function(*): {name: *, type: string, validate: function(*): *}),
|
|
1193
|
+
* optional: (function(*): *&{optional: boolean}),
|
|
1194
|
+
* oneOf: (function(*, ...[*]): {name: *, type: string, types: *[],
|
|
1195
|
+
* validate: function(*): boolean})
|
|
1196
|
+
* }}
|
|
1197
|
+
*/
|
|
1198
|
+
const ArgTypes = {
|
|
1199
|
+
string: (name) => ({ name, type: 'string', validate: (v) => Validator.isString(v) }),
|
|
1200
|
+
number: (name) => ({ name, type: 'number', validate: (v) => Validator.isNumber(v) }),
|
|
1201
|
+
boolean: (name) => ({ name, type: 'boolean', validate: (v) => Validator.isBoolean(v) }),
|
|
1202
|
+
observable: (name) => ({ name, type: 'observable', validate: (v) => Validator.isObservable(v) }),
|
|
1203
|
+
element: (name) => ({ name, type: 'element', validate: (v) => Validator.isElement(v) }),
|
|
1204
|
+
function: (name) => ({ name, type: 'function', validate: (v) => Validator.isFunction(v) }),
|
|
1205
|
+
object: (name) => ({ name, type: 'object', validate: (v) => (Validator.isObject(v)) }),
|
|
1206
|
+
objectNotNull: (name) => ({ name, type: 'object', validate: (v) => (Validator.isObject(v) && v !== null) }),
|
|
1207
|
+
children: (name) => ({ name, type: 'children', validate: (v) => Validator.validateChildren(v) }),
|
|
1208
|
+
attributes: (name) => ({ name, type: 'attributes', validate: (v) => Validator.validateAttributes(v) }),
|
|
1209
|
+
|
|
1210
|
+
// Optional arguments
|
|
1211
|
+
optional: (argType) => ({ ...argType, optional: true }),
|
|
1212
|
+
|
|
1213
|
+
// Union types
|
|
1214
|
+
oneOf: (name, ...argTypes) => ({
|
|
1215
|
+
name,
|
|
1216
|
+
type: 'oneOf',
|
|
1217
|
+
types: argTypes,
|
|
1218
|
+
validate: (v) => argTypes.some(type => type.validate(v))
|
|
1219
|
+
})
|
|
1220
|
+
};
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
*
|
|
1224
|
+
* @param {Array} args
|
|
1225
|
+
* @param {Array} argSchema
|
|
1226
|
+
* @param {string} fnName
|
|
1227
|
+
*/
|
|
1228
|
+
const validateArgs = (args, argSchema, fnName = 'Function') => {
|
|
1229
|
+
if (!argSchema) return;
|
|
1230
|
+
|
|
1231
|
+
const errors = [];
|
|
1232
|
+
|
|
1233
|
+
// Check the number of arguments
|
|
1234
|
+
const requiredCount = argSchema.filter(arg => !arg.optional).length;
|
|
1235
|
+
if (args.length < requiredCount) {
|
|
1236
|
+
errors.push(`${fnName}: Expected at least ${requiredCount} arguments, got ${args.length}`);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// Validate each argument
|
|
1240
|
+
argSchema.forEach((schema, index) => {
|
|
1241
|
+
const position = index + 1;
|
|
1242
|
+
const value = args[index];
|
|
1243
|
+
|
|
1244
|
+
if (value === undefined) {
|
|
1245
|
+
if (!schema.optional) {
|
|
1246
|
+
errors.push(`${fnName}: Missing required argument '${schema.name}' at position ${position}`);
|
|
1247
|
+
}
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
if (!schema.validate(value)) {
|
|
1252
|
+
const valueTypeOf = value?.constructor?.name || typeof value;
|
|
1253
|
+
errors.push(`${fnName}: Invalid argument '${schema.name}' at position ${position}, expected ${schema.type}, got ${valueTypeOf}`);
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
if (errors.length > 0) {
|
|
1258
|
+
throw new ArgTypesError(`Argument validation failed`, errors);
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1261
|
+
|
|
1262
|
+
/**
|
|
1263
|
+
* @param {Function} fn
|
|
1264
|
+
* @param {Array} argSchema
|
|
1265
|
+
* @param {string} fnName
|
|
1266
|
+
* @returns {Function}
|
|
1267
|
+
*/
|
|
1268
|
+
const withValidation = (fn, argSchema, fnName = 'Function') => {
|
|
1269
|
+
if(!Validator.isArray(argSchema)) {
|
|
1270
|
+
throw new NativeDocumentError('withValidation : argSchema must be an array');
|
|
1271
|
+
}
|
|
1272
|
+
return function(...args) {
|
|
1273
|
+
validateArgs(args, argSchema, fn.name || fnName);
|
|
1274
|
+
return fn.apply(this, args);
|
|
1275
|
+
};
|
|
1276
|
+
};
|
|
1277
|
+
|
|
1278
|
+
Function.prototype.args = function(...args) {
|
|
1279
|
+
return withValidation(this, args);
|
|
1280
|
+
};
|
|
1281
|
+
|
|
1282
|
+
Function.prototype.errorBoundary = function(callback) {
|
|
1283
|
+
return (...args) => {
|
|
1284
|
+
try {
|
|
1285
|
+
return this.apply(this, args);
|
|
1286
|
+
} catch(e) {
|
|
1287
|
+
return callback(e);
|
|
1288
|
+
}
|
|
1289
|
+
};
|
|
1290
|
+
};
|
|
1291
|
+
|
|
1292
|
+
String.prototype.use = function(args) {
|
|
1293
|
+
const value = this;
|
|
1294
|
+
|
|
1295
|
+
return Observable.computed(() => {
|
|
1296
|
+
return value.replace(/\$\{(.*?)}/g, (match, key) => {
|
|
1297
|
+
const data = args[key];
|
|
1298
|
+
if(Validator.isObservable(data)) {
|
|
1299
|
+
return data.val();
|
|
1300
|
+
}
|
|
1301
|
+
return data;
|
|
1302
|
+
});
|
|
1303
|
+
}, Object.values(args));
|
|
1304
|
+
};
|
|
1305
|
+
|
|
1306
|
+
String.prototype.resolveObservableTemplate = function() {
|
|
1307
|
+
if(!Validator.containsObservableReference(this)) {
|
|
1308
|
+
return this;
|
|
1309
|
+
}
|
|
1310
|
+
return this.split(/(\{\{#ObItem::\([0-9]+\)\}\})/g).filter(Boolean).map((value) => {
|
|
1311
|
+
if(!Validator.containsObservableReference(value)) {
|
|
1312
|
+
return value;
|
|
1313
|
+
}
|
|
1314
|
+
const [_, id] = value.match(/\{\{#ObItem::\(([0-9]+)\)\}\}/);
|
|
1315
|
+
return Observable.getById(id);
|
|
1316
|
+
});
|
|
1317
|
+
};
|
|
1318
|
+
|
|
1319
|
+
const Store = (function() {
|
|
1320
|
+
|
|
1321
|
+
const $stores = new Map();
|
|
1322
|
+
|
|
1323
|
+
return {
|
|
1324
|
+
/**
|
|
1325
|
+
* Create a new state follower and return it.
|
|
1326
|
+
* @param {string} name
|
|
1327
|
+
* @returns {ObservableItem}
|
|
1328
|
+
*/
|
|
1329
|
+
use(name) {
|
|
1330
|
+
const {observer: originalObserver, subscribers } = $stores.get(name);
|
|
1331
|
+
const observerFollower = Observable(originalObserver.val());
|
|
1332
|
+
const unSubscriber = originalObserver.subscribe(value => observerFollower.set(value));
|
|
1333
|
+
const updaterUnsubscriber = observerFollower.subscribe(value => originalObserver.set(value));
|
|
1334
|
+
observerFollower.destroy = () => {
|
|
1335
|
+
unSubscriber();
|
|
1336
|
+
updaterUnsubscriber();
|
|
1337
|
+
observerFollower.cleanup();
|
|
1338
|
+
};
|
|
1339
|
+
subscribers.add(observerFollower);
|
|
1340
|
+
|
|
1341
|
+
return observerFollower;
|
|
1342
|
+
},
|
|
1343
|
+
/**
|
|
1344
|
+
* @param {string} name
|
|
1345
|
+
* @returns {ObservableItem}
|
|
1346
|
+
*/
|
|
1347
|
+
follow(name) {
|
|
1348
|
+
return this.use(name);
|
|
1349
|
+
},
|
|
1350
|
+
/**
|
|
1351
|
+
* Create a new state and return the observer.
|
|
1352
|
+
* @param {string} name
|
|
1353
|
+
* @param {*} value
|
|
1354
|
+
* @returns {ObservableItem}
|
|
1355
|
+
*/
|
|
1356
|
+
create(name, value) {
|
|
1357
|
+
const observer = Observable(value);
|
|
1358
|
+
$stores.set(name, { observer, subscribers: new Set()});
|
|
1359
|
+
return observer;
|
|
1360
|
+
},
|
|
1361
|
+
/**
|
|
1362
|
+
* Get the observer for a state.
|
|
1363
|
+
* @param {string} name
|
|
1364
|
+
* @returns {null|ObservableItem}
|
|
1365
|
+
*/
|
|
1366
|
+
get(name) {
|
|
1367
|
+
const item = $stores.get(name);
|
|
1368
|
+
return item ? item.observer : null;
|
|
1369
|
+
},
|
|
1370
|
+
/**
|
|
1371
|
+
*
|
|
1372
|
+
* @param {string} name
|
|
1373
|
+
* @returns {{observer: ObservableItem, subscribers: Set}}
|
|
1374
|
+
*/
|
|
1375
|
+
getWithSubscribers(name) {
|
|
1376
|
+
return $stores.get(name);
|
|
1377
|
+
},
|
|
1378
|
+
/**
|
|
1379
|
+
* Delete a state.
|
|
1380
|
+
* @param {string} name
|
|
1381
|
+
*/
|
|
1382
|
+
delete(name) {
|
|
1383
|
+
const item = $stores.get(name);
|
|
1384
|
+
if(!item) return;
|
|
1385
|
+
item.observer.cleanup();
|
|
1386
|
+
item.subscribers.forEach(follower => follower.destroy());
|
|
1387
|
+
item.observer.clear();
|
|
1388
|
+
}
|
|
1389
|
+
};
|
|
1390
|
+
}());
|
|
1391
|
+
|
|
1392
|
+
/**
|
|
1393
|
+
*
|
|
1394
|
+
* @param {*} item
|
|
1395
|
+
* @param {string|null} defaultKey
|
|
1396
|
+
* @param {?Function} key
|
|
1397
|
+
* @returns {*}
|
|
1398
|
+
*/
|
|
1399
|
+
const getKey = (item, defaultKey, key) => {
|
|
1400
|
+
if(Validator.isFunction(key)) return key(item, defaultKey);
|
|
1401
|
+
if(Validator.isObservable(item)) {
|
|
1402
|
+
const val = item.val();
|
|
1403
|
+
return (val && key) ? val[key] : defaultKey;
|
|
1404
|
+
}
|
|
1405
|
+
return item[key] ?? defaultKey;
|
|
1406
|
+
};
|
|
1407
|
+
|
|
1408
|
+
/**
|
|
1409
|
+
*
|
|
1410
|
+
* @param {Map} cache
|
|
1411
|
+
* @param {Set} keyIds
|
|
1412
|
+
*/
|
|
1413
|
+
const cleanBlockByCache = (cache, keyIds) => {
|
|
1414
|
+
for(const [key, {child}] of cache.entries()) {
|
|
1415
|
+
if(keyIds.has(key)) {
|
|
1416
|
+
continue;
|
|
1417
|
+
}
|
|
1418
|
+
child.remove();
|
|
1419
|
+
}
|
|
1420
|
+
};
|
|
1421
|
+
|
|
1422
|
+
/**
|
|
1423
|
+
*
|
|
1424
|
+
* @param {Array|Object|ObservableItem} data
|
|
1425
|
+
* @param {Function} callback
|
|
1426
|
+
* @param {?Function} key
|
|
1427
|
+
* @returns {DocumentFragment}
|
|
1428
|
+
*/
|
|
1429
|
+
function ForEach(data, callback, key) {
|
|
1430
|
+
const element = new Anchor('ForEach');
|
|
1431
|
+
const blockEnd = element.endElement();
|
|
1432
|
+
|
|
1433
|
+
let cache = new Map();
|
|
1434
|
+
|
|
1435
|
+
const handleContentItem = (item, indexKey) => {
|
|
1436
|
+
const keyId = getKey(item, indexKey, key);
|
|
1437
|
+
|
|
1438
|
+
if(cache.has(keyId)) {
|
|
1439
|
+
cache.get(keyId).indexObserver.set(indexKey);
|
|
1440
|
+
}
|
|
1441
|
+
else {
|
|
1442
|
+
const indexObserver = Observable(indexKey);
|
|
1443
|
+
let child = callback(item, indexObserver);
|
|
1444
|
+
if(Validator.isStringOrObservable(child)) {
|
|
1445
|
+
child = createTextNode(child);
|
|
1446
|
+
}
|
|
1447
|
+
cache.set(keyId, { child, indexObserver});
|
|
1448
|
+
}
|
|
1449
|
+
return keyId;
|
|
1450
|
+
};
|
|
1451
|
+
const keyIds = new Set();
|
|
1452
|
+
|
|
1453
|
+
const buildContent = () => {
|
|
1454
|
+
const items = (Validator.isObservable(data)) ? data.val() : data;
|
|
1455
|
+
const parent = blockEnd.parentNode;
|
|
1456
|
+
if(!parent) {
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
keyIds.clear();
|
|
1460
|
+
if(Array.isArray(items)) {
|
|
1461
|
+
items.forEach((item, index) => keyIds.add(handleContentItem(item, index)));
|
|
1462
|
+
} else {
|
|
1463
|
+
for(const indexKey in items) {
|
|
1464
|
+
keyIds.add(handleContentItem(items[indexKey], indexKey));
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
cleanBlockByCache(cache, keyIds);
|
|
1469
|
+
let nextElementSibling = blockEnd;
|
|
1470
|
+
for(const item of [...keyIds].reverse()) {
|
|
1471
|
+
const { child } = cache.get(item);
|
|
1472
|
+
if(child) {
|
|
1473
|
+
if(nextElementSibling && nextElementSibling.previousSibling === child) {
|
|
1474
|
+
nextElementSibling = child;
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1477
|
+
parent.insertBefore(child, nextElementSibling);
|
|
1478
|
+
nextElementSibling = child;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
};
|
|
1482
|
+
|
|
1483
|
+
buildContent();
|
|
1484
|
+
if(Validator.isObservable(data)) {
|
|
1485
|
+
data.subscribe(throttle((newValue, oldValue) => {
|
|
1486
|
+
buildContent();
|
|
1487
|
+
}, 50, { debounce: true }));
|
|
1488
|
+
}
|
|
1489
|
+
return element;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/**
|
|
1493
|
+
* Show the element if the condition is true
|
|
1494
|
+
*
|
|
1495
|
+
* @param {ObservableItem|ObservableChecker} condition
|
|
1496
|
+
* @param {*} child
|
|
1497
|
+
* @param {string|null} comment
|
|
1498
|
+
* @returns {DocumentFragment}
|
|
1499
|
+
*/
|
|
1500
|
+
const ShowIf = function(condition, child, comment = null) {
|
|
1501
|
+
if(!(Validator.isObservable(condition))) {
|
|
1502
|
+
return DebugManager.warn('ShowIf', "ShowIf : condition must be an Observable / "+comment, condition);
|
|
1503
|
+
}
|
|
1504
|
+
const element = new Anchor('Show if : '+(comment || ''));
|
|
1505
|
+
|
|
1506
|
+
let childElement = null;
|
|
1507
|
+
const getChildElement = () => {
|
|
1508
|
+
if(childElement) {
|
|
1509
|
+
return childElement;
|
|
1510
|
+
}
|
|
1511
|
+
if(typeof child === 'function') {
|
|
1512
|
+
childElement = child();
|
|
1513
|
+
}
|
|
1514
|
+
else {
|
|
1515
|
+
childElement = child;
|
|
1516
|
+
}
|
|
1517
|
+
if(Validator.isStringOrObservable(childElement)) {
|
|
1518
|
+
childElement = createTextNode(childElement);
|
|
1519
|
+
}
|
|
1520
|
+
return childElement;
|
|
1521
|
+
};
|
|
1522
|
+
|
|
1523
|
+
const currentValue = condition.val();
|
|
1524
|
+
|
|
1525
|
+
if(currentValue) {
|
|
1526
|
+
element.appendChild(getChildElement());
|
|
1527
|
+
}
|
|
1528
|
+
condition.subscribe(value => {
|
|
1529
|
+
if(value) {
|
|
1530
|
+
element.appendChild(getChildElement());
|
|
1531
|
+
} else {
|
|
1532
|
+
element.remove();
|
|
1533
|
+
}
|
|
1534
|
+
});
|
|
1535
|
+
|
|
1536
|
+
return element;
|
|
1537
|
+
};
|
|
1538
|
+
|
|
1539
|
+
/**
|
|
1540
|
+
* Hide the element if the condition is true
|
|
1541
|
+
* @param {ObservableItem|ObservableChecker} condition
|
|
1542
|
+
* @param child
|
|
1543
|
+
* @param comment
|
|
1544
|
+
* @returns {DocumentFragment}
|
|
1545
|
+
*/
|
|
1546
|
+
const HideIf = function(condition, child, comment) {
|
|
1547
|
+
|
|
1548
|
+
const hideCondition = Observable(!condition.val());
|
|
1549
|
+
condition.subscribe(value => hideCondition.set(!value));
|
|
1550
|
+
|
|
1551
|
+
return ShowIf(hideCondition, child, comment);
|
|
1552
|
+
};
|
|
1553
|
+
|
|
1554
|
+
/**
|
|
1555
|
+
* Hide the element if the condition is false
|
|
1556
|
+
*
|
|
1557
|
+
* @param {ObservableItem|ObservableChecker} condition
|
|
1558
|
+
* @param {*} child
|
|
1559
|
+
* @param {string|null} comment
|
|
1560
|
+
* @returns {DocumentFragment}
|
|
1561
|
+
*/
|
|
1562
|
+
const HideIfNot = function(condition, child, comment) {
|
|
1563
|
+
return ShowIf(condition, child, comment);
|
|
1564
|
+
};
|
|
1565
|
+
|
|
1566
|
+
/**
|
|
1567
|
+
*
|
|
1568
|
+
* @param {ObservableItem|ObservableChecker} $condition
|
|
1569
|
+
* @param {{[key]: *}} values
|
|
1570
|
+
* @returns {DocumentFragment}
|
|
1571
|
+
*/
|
|
1572
|
+
const Match = function($condition, values) {
|
|
1573
|
+
|
|
1574
|
+
if(!Validator.isObservable($condition)) {
|
|
1575
|
+
throw new NativeDocumentError("Toggle : condition must be an Observable");
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
const anchor = new Anchor();
|
|
1579
|
+
const cache = new Map();
|
|
1580
|
+
|
|
1581
|
+
const getItem = function(key) {
|
|
1582
|
+
if(cache.has(key)) {
|
|
1583
|
+
return cache.get(key);
|
|
1584
|
+
}
|
|
1585
|
+
let item = values[key];
|
|
1586
|
+
if(!item) {
|
|
1587
|
+
return null;
|
|
1588
|
+
}
|
|
1589
|
+
if(Validator.isFunction(item)) {
|
|
1590
|
+
item = item();
|
|
1591
|
+
}
|
|
1592
|
+
cache.set(key, item);
|
|
1593
|
+
return item;
|
|
1594
|
+
};
|
|
1595
|
+
|
|
1596
|
+
const defaultValue = $condition.val();
|
|
1597
|
+
const defaultContent = getItem(defaultValue);
|
|
1598
|
+
if(defaultContent) {
|
|
1599
|
+
anchor.appendChild(defaultContent);
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
$condition.subscribe(value => {
|
|
1603
|
+
const content = getItem(value);
|
|
1604
|
+
anchor.remove();
|
|
1605
|
+
if(content) {
|
|
1606
|
+
anchor.appendChild(content);
|
|
1607
|
+
}
|
|
1608
|
+
});
|
|
1609
|
+
|
|
1610
|
+
return anchor;
|
|
1611
|
+
};
|
|
1612
|
+
|
|
1613
|
+
|
|
1614
|
+
/**
|
|
1615
|
+
*
|
|
1616
|
+
* @param {ObservableItem|ObservableChecker} $condition
|
|
1617
|
+
* @param {*} onTrue
|
|
1618
|
+
* @param {*} onFalse
|
|
1619
|
+
* @returns {DocumentFragment}
|
|
1620
|
+
*/
|
|
1621
|
+
const Switch = function ($condition, onTrue, onFalse) {
|
|
1622
|
+
|
|
1623
|
+
if(!Validator.isObservable($condition)) {
|
|
1624
|
+
throw new NativeDocumentError("Toggle : condition must be an Observable");
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
return Match($condition, {
|
|
1628
|
+
true: onTrue,
|
|
1629
|
+
false: onFalse,
|
|
1630
|
+
});
|
|
1631
|
+
};
|
|
1632
|
+
|
|
1633
|
+
/**
|
|
1634
|
+
*
|
|
1635
|
+
* @param {ObservableItem|ObservableChecker} $condition
|
|
1636
|
+
* @returns {{show: Function, otherwise: (((*) => {}):DocumentFragment)}
|
|
1637
|
+
*/
|
|
1638
|
+
const When = function($condition) {
|
|
1639
|
+
if(!Validator.isObservable($condition)) {
|
|
1640
|
+
throw new NativeDocumentError("When : condition must be an Observable");
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
let $onTrue = null;
|
|
1644
|
+
let $onFalse = null;
|
|
1645
|
+
|
|
1646
|
+
return {
|
|
1647
|
+
show(onTrue) {
|
|
1648
|
+
$onTrue = onTrue;
|
|
1649
|
+
return this;
|
|
1650
|
+
},
|
|
1651
|
+
otherwise(onFalse) {
|
|
1652
|
+
$onFalse = onFalse;
|
|
1653
|
+
return Switch($condition, $onTrue, $onFalse);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
};
|
|
1657
|
+
|
|
1658
|
+
const Div = HtmlElementWrapper('div');
|
|
1659
|
+
const Span = HtmlElementWrapper('span');
|
|
1660
|
+
const Label = HtmlElementWrapper('label');
|
|
1661
|
+
const P = HtmlElementWrapper('p');
|
|
1662
|
+
const Paragraph = P;
|
|
1663
|
+
const Strong = HtmlElementWrapper('strong');
|
|
1664
|
+
const H1 = HtmlElementWrapper('h1');
|
|
1665
|
+
const H2 = HtmlElementWrapper('h2');
|
|
1666
|
+
const H3 = HtmlElementWrapper('h3');
|
|
1667
|
+
const H4 = HtmlElementWrapper('h4');
|
|
1668
|
+
const H5 = HtmlElementWrapper('h5');
|
|
1669
|
+
const H6 = HtmlElementWrapper('h6');
|
|
1670
|
+
|
|
1671
|
+
const Br = HtmlElementWrapper('br');
|
|
1672
|
+
|
|
1673
|
+
const Link$1 = HtmlElementWrapper('a');
|
|
1674
|
+
const Pre = HtmlElementWrapper('pre');
|
|
1675
|
+
const Code = HtmlElementWrapper('code');
|
|
1676
|
+
const Blockquote = HtmlElementWrapper('blockquote');
|
|
1677
|
+
const Hr = HtmlElementWrapper('hr');
|
|
1678
|
+
const Em = HtmlElementWrapper('em');
|
|
1679
|
+
const Small = HtmlElementWrapper('small');
|
|
1680
|
+
const Mark = HtmlElementWrapper('mark');
|
|
1681
|
+
const Del = HtmlElementWrapper('del');
|
|
1682
|
+
const Ins = HtmlElementWrapper('ins');
|
|
1683
|
+
const Sub = HtmlElementWrapper('sub');
|
|
1684
|
+
const Sup = HtmlElementWrapper('sup');
|
|
1685
|
+
const Abbr = HtmlElementWrapper('abbr');
|
|
1686
|
+
const Cite = HtmlElementWrapper('cite');
|
|
1687
|
+
const Quote = HtmlElementWrapper('q');
|
|
1688
|
+
|
|
1689
|
+
const Dl = HtmlElementWrapper('dl');
|
|
1690
|
+
const Dt = HtmlElementWrapper('dt');
|
|
1691
|
+
const Dd = HtmlElementWrapper('dd');
|
|
1692
|
+
|
|
1693
|
+
const Form = HtmlElementWrapper('form', function(el) {
|
|
1694
|
+
|
|
1695
|
+
el.submit = function(action) {
|
|
1696
|
+
if(typeof action === 'function') {
|
|
1697
|
+
el.on.submit((e) => {
|
|
1698
|
+
e.preventDefault();
|
|
1699
|
+
action(e);
|
|
1700
|
+
});
|
|
1701
|
+
return el;
|
|
1702
|
+
}
|
|
1703
|
+
this.setAttribute('action', action);
|
|
1704
|
+
return el;
|
|
1705
|
+
};
|
|
1706
|
+
el.multipartFormData = function() {
|
|
1707
|
+
this.setAttribute('enctype', 'multipart/form-data');
|
|
1708
|
+
return el;
|
|
1709
|
+
};
|
|
1710
|
+
el.post = function(action) {
|
|
1711
|
+
this.setAttribute('method', 'post');
|
|
1712
|
+
this.setAttribute('action', action);
|
|
1713
|
+
return el;
|
|
1714
|
+
};
|
|
1715
|
+
el.get = function(action) {
|
|
1716
|
+
this.setAttribute('method', 'get');
|
|
1717
|
+
this.setAttribute('action', action);
|
|
1718
|
+
};
|
|
1719
|
+
return el;
|
|
1720
|
+
});
|
|
1721
|
+
|
|
1722
|
+
const Input = HtmlElementWrapper('input');
|
|
1723
|
+
|
|
1724
|
+
const TextArea = HtmlElementWrapper('textarea');
|
|
1725
|
+
const TextInput = TextArea;
|
|
1726
|
+
|
|
1727
|
+
const Select = HtmlElementWrapper('select');
|
|
1728
|
+
const FieldSet = HtmlElementWrapper('fieldset', );
|
|
1729
|
+
const Option = HtmlElementWrapper('option');
|
|
1730
|
+
const Legend = HtmlElementWrapper('legend');
|
|
1731
|
+
const Datalist = HtmlElementWrapper('datalist');
|
|
1732
|
+
const Output = HtmlElementWrapper('output');
|
|
1733
|
+
const Progress = HtmlElementWrapper('progress');
|
|
1734
|
+
const Meter = HtmlElementWrapper('meter');
|
|
1735
|
+
|
|
1736
|
+
const ReadonlyInput = (attributes) => Input({ readonly: true, ...attributes });
|
|
1737
|
+
const HiddenInput = (attributes) => Input({type: 'hidden', ...attributes });
|
|
1738
|
+
const FileInput = (attributes) => Input({ type: 'file', ...attributes });
|
|
1739
|
+
const PasswordInput = (attributes) => Input({ type: 'password', ...attributes });
|
|
1740
|
+
const Checkbox = (attributes) => Input({ type: 'checkbox', ...attributes });
|
|
1741
|
+
const Radio = (attributes) => Input({ type: 'radio', ...attributes });
|
|
1742
|
+
|
|
1743
|
+
const RangeInput = (attributes) => Input({ type: 'range', ...attributes });
|
|
1744
|
+
const ColorInput = (attributes) => Input({ type: 'color', ...attributes });
|
|
1745
|
+
const DateInput = (attributes) => Input({ type: 'date', ...attributes });
|
|
1746
|
+
const TimeInput = (attributes) => Input({ type: 'time', ...attributes });
|
|
1747
|
+
const DateTimeInput = (attributes) => Input({ type: 'datetime-local', ...attributes });
|
|
1748
|
+
const WeekInput = (attributes) => Input({ type: 'week', ...attributes });
|
|
1749
|
+
const MonthInput = (attributes) => Input({ type: 'month', ...attributes });
|
|
1750
|
+
const SearchInput = (attributes) => Input({ type: 'search', ...attributes });
|
|
1751
|
+
const TelInput = (attributes) => Input({ type: 'tel', ...attributes });
|
|
1752
|
+
const UrlInput = (attributes) => Input({ type: 'url', ...attributes });
|
|
1753
|
+
const EmailInput = (attributes) => Input({ type: 'email', ...attributes });
|
|
1754
|
+
const NumberInput = (attributes) => Input({ type: 'number', ...attributes });
|
|
1755
|
+
|
|
1756
|
+
|
|
1757
|
+
const Button = HtmlElementWrapper('button');
|
|
1758
|
+
const SimpleButton = (child, attributes) => Button(child, { type: 'button', ...attributes });
|
|
1759
|
+
const SubmitButton = (child, attributes) => Button(child, { type: 'submit', ...attributes });
|
|
1760
|
+
|
|
1761
|
+
const Main = HtmlElementWrapper('main');
|
|
1762
|
+
const Section = HtmlElementWrapper('section');
|
|
1763
|
+
const Article = HtmlElementWrapper('article');
|
|
1764
|
+
const Aside = HtmlElementWrapper('aside');
|
|
1765
|
+
const Nav = HtmlElementWrapper('nav');
|
|
1766
|
+
const Figure = HtmlElementWrapper('figure');
|
|
1767
|
+
const FigCaption = HtmlElementWrapper('figcaption');
|
|
1768
|
+
|
|
1769
|
+
const Header = HtmlElementWrapper('header');
|
|
1770
|
+
const Footer = HtmlElementWrapper('footer');
|
|
1771
|
+
|
|
1772
|
+
const BaseImage = HtmlElementWrapper('img');
|
|
1773
|
+
const Img = function(src, attributes) {
|
|
1774
|
+
return BaseImage({ src, ...attributes });
|
|
1775
|
+
};
|
|
1776
|
+
|
|
1777
|
+
/**
|
|
1778
|
+
*
|
|
1779
|
+
* @param {string} src
|
|
1780
|
+
* @param {string|null} defaultImage
|
|
1781
|
+
* @param {Object} attributes
|
|
1782
|
+
* @param {?Function} callback
|
|
1783
|
+
* @returns {Image}
|
|
1784
|
+
*/
|
|
1785
|
+
const AsyncImg = function(src, defaultImage, attributes, callback) {
|
|
1786
|
+
const image = Img(defaultImage || src, attributes);
|
|
1787
|
+
const img = new Image();
|
|
1788
|
+
img.onload = () => {
|
|
1789
|
+
Validator.isFunction(callback) && callback(null, image);
|
|
1790
|
+
image.src = src;
|
|
1791
|
+
};
|
|
1792
|
+
img.onerror = () => {
|
|
1793
|
+
Validator.isFunction(callback) && callback(new NativeDocumentError('Image not found'));
|
|
1794
|
+
};
|
|
1795
|
+
if(Validator.isObservable(src)) {
|
|
1796
|
+
src.subscribe(newSrc => {
|
|
1797
|
+
img.src = newSrc;
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
img.src = src;
|
|
1801
|
+
return image;
|
|
1802
|
+
};
|
|
1803
|
+
|
|
1804
|
+
/**
|
|
1805
|
+
*
|
|
1806
|
+
* @param {string} src
|
|
1807
|
+
* @param {Object} attributes
|
|
1808
|
+
* @returns {Image}
|
|
1809
|
+
*/
|
|
1810
|
+
const LazyImg = function(src, attributes) {
|
|
1811
|
+
return Img(src, { ...attributes, loading: 'lazy' });
|
|
1812
|
+
};
|
|
1813
|
+
|
|
1814
|
+
const Details = HtmlElementWrapper('details');
|
|
1815
|
+
const Summary = HtmlElementWrapper('summary');
|
|
1816
|
+
const Dialog = HtmlElementWrapper('dialog');
|
|
1817
|
+
const Menu = HtmlElementWrapper('menu');
|
|
1818
|
+
|
|
1819
|
+
const OrderedList = HtmlElementWrapper('ol');
|
|
1820
|
+
const UnorderedList = HtmlElementWrapper('ul');
|
|
1821
|
+
const ListItem = HtmlElementWrapper('li');
|
|
1822
|
+
|
|
1823
|
+
const Audio = HtmlElementWrapper('audio');
|
|
1824
|
+
const Video = HtmlElementWrapper('video');
|
|
1825
|
+
const Source = HtmlElementWrapper('source');
|
|
1826
|
+
const Track = HtmlElementWrapper('track');
|
|
1827
|
+
const Canvas = HtmlElementWrapper('canvas');
|
|
1828
|
+
const Svg = HtmlElementWrapper('svg');
|
|
1829
|
+
|
|
1830
|
+
const Time = HtmlElementWrapper('time');
|
|
1831
|
+
const Data = HtmlElementWrapper('data');
|
|
1832
|
+
const Address = HtmlElementWrapper('address');
|
|
1833
|
+
const Kbd = HtmlElementWrapper('kbd');
|
|
1834
|
+
const Samp = HtmlElementWrapper('samp');
|
|
1835
|
+
const Var = HtmlElementWrapper('var');
|
|
1836
|
+
const Wbr = HtmlElementWrapper('wbr');
|
|
1837
|
+
|
|
1838
|
+
const Caption = HtmlElementWrapper('caption');
|
|
1839
|
+
const Table = HtmlElementWrapper('table');
|
|
1840
|
+
const THead = HtmlElementWrapper('thead');
|
|
1841
|
+
const TFoot = HtmlElementWrapper('tfoot');
|
|
1842
|
+
const TBody = HtmlElementWrapper('tbody');
|
|
1843
|
+
const Tr = HtmlElementWrapper('tr');
|
|
1844
|
+
const TRow = Tr;
|
|
1845
|
+
const Th = HtmlElementWrapper('th');
|
|
1846
|
+
const THeadCell = Th;
|
|
1847
|
+
const TFootCell = Th;
|
|
1848
|
+
const Td = HtmlElementWrapper('td');
|
|
1849
|
+
const TBodyCell = Td;
|
|
1850
|
+
|
|
1851
|
+
const Fragment = HtmlElementWrapper('');
|
|
1852
|
+
|
|
1853
|
+
var elements = /*#__PURE__*/Object.freeze({
|
|
1854
|
+
__proto__: null,
|
|
1855
|
+
Abbr: Abbr,
|
|
1856
|
+
Address: Address,
|
|
1857
|
+
Anchor: Anchor,
|
|
1858
|
+
Article: Article,
|
|
1859
|
+
Aside: Aside,
|
|
1860
|
+
AsyncImg: AsyncImg,
|
|
1861
|
+
Audio: Audio,
|
|
1862
|
+
BaseImage: BaseImage,
|
|
1863
|
+
Blockquote: Blockquote,
|
|
1864
|
+
Br: Br,
|
|
1865
|
+
Button: Button,
|
|
1866
|
+
Canvas: Canvas,
|
|
1867
|
+
Caption: Caption,
|
|
1868
|
+
Checkbox: Checkbox,
|
|
1869
|
+
Cite: Cite,
|
|
1870
|
+
Code: Code,
|
|
1871
|
+
ColorInput: ColorInput,
|
|
1872
|
+
Data: Data,
|
|
1873
|
+
Datalist: Datalist,
|
|
1874
|
+
DateInput: DateInput,
|
|
1875
|
+
DateTimeInput: DateTimeInput,
|
|
1876
|
+
Dd: Dd,
|
|
1877
|
+
Del: Del,
|
|
1878
|
+
Details: Details,
|
|
1879
|
+
Dialog: Dialog,
|
|
1880
|
+
Div: Div,
|
|
1881
|
+
Dl: Dl,
|
|
1882
|
+
Dt: Dt,
|
|
1883
|
+
Em: Em,
|
|
1884
|
+
EmailInput: EmailInput,
|
|
1885
|
+
FieldSet: FieldSet,
|
|
1886
|
+
FigCaption: FigCaption,
|
|
1887
|
+
Figure: Figure,
|
|
1888
|
+
FileInput: FileInput,
|
|
1889
|
+
Footer: Footer,
|
|
1890
|
+
ForEach: ForEach,
|
|
1891
|
+
Form: Form,
|
|
1892
|
+
Fragment: Fragment,
|
|
1893
|
+
H1: H1,
|
|
1894
|
+
H2: H2,
|
|
1895
|
+
H3: H3,
|
|
1896
|
+
H4: H4,
|
|
1897
|
+
H5: H5,
|
|
1898
|
+
H6: H6,
|
|
1899
|
+
Header: Header,
|
|
1900
|
+
HiddenInput: HiddenInput,
|
|
1901
|
+
HideIf: HideIf,
|
|
1902
|
+
HideIfNot: HideIfNot,
|
|
1903
|
+
Hr: Hr,
|
|
1904
|
+
Img: Img,
|
|
1905
|
+
Input: Input,
|
|
1906
|
+
Ins: Ins,
|
|
1907
|
+
Kbd: Kbd,
|
|
1908
|
+
Label: Label,
|
|
1909
|
+
LazyImg: LazyImg,
|
|
1910
|
+
Legend: Legend,
|
|
1911
|
+
Link: Link$1,
|
|
1912
|
+
ListItem: ListItem,
|
|
1913
|
+
Main: Main,
|
|
1914
|
+
Mark: Mark,
|
|
1915
|
+
Match: Match,
|
|
1916
|
+
Menu: Menu,
|
|
1917
|
+
Meter: Meter,
|
|
1918
|
+
MonthInput: MonthInput,
|
|
1919
|
+
Nav: Nav,
|
|
1920
|
+
NumberInput: NumberInput,
|
|
1921
|
+
Option: Option,
|
|
1922
|
+
OrderedList: OrderedList,
|
|
1923
|
+
Output: Output,
|
|
1924
|
+
P: P,
|
|
1925
|
+
Paragraph: Paragraph,
|
|
1926
|
+
PasswordInput: PasswordInput,
|
|
1927
|
+
Pre: Pre,
|
|
1928
|
+
Progress: Progress,
|
|
1929
|
+
Quote: Quote,
|
|
1930
|
+
Radio: Radio,
|
|
1931
|
+
RangeInput: RangeInput,
|
|
1932
|
+
ReadonlyInput: ReadonlyInput,
|
|
1933
|
+
Samp: Samp,
|
|
1934
|
+
SearchInput: SearchInput,
|
|
1935
|
+
Section: Section,
|
|
1936
|
+
Select: Select,
|
|
1937
|
+
ShowIf: ShowIf,
|
|
1938
|
+
SimpleButton: SimpleButton,
|
|
1939
|
+
Small: Small,
|
|
1940
|
+
Source: Source,
|
|
1941
|
+
Span: Span,
|
|
1942
|
+
Strong: Strong,
|
|
1943
|
+
Sub: Sub,
|
|
1944
|
+
SubmitButton: SubmitButton,
|
|
1945
|
+
Summary: Summary,
|
|
1946
|
+
Sup: Sup,
|
|
1947
|
+
Svg: Svg,
|
|
1948
|
+
Switch: Switch,
|
|
1949
|
+
TBody: TBody,
|
|
1950
|
+
TBodyCell: TBodyCell,
|
|
1951
|
+
TFoot: TFoot,
|
|
1952
|
+
TFootCell: TFootCell,
|
|
1953
|
+
THead: THead,
|
|
1954
|
+
THeadCell: THeadCell,
|
|
1955
|
+
TRow: TRow,
|
|
1956
|
+
Table: Table,
|
|
1957
|
+
Td: Td,
|
|
1958
|
+
TelInput: TelInput,
|
|
1959
|
+
TextArea: TextArea,
|
|
1960
|
+
TextInput: TextInput,
|
|
1961
|
+
Th: Th,
|
|
1962
|
+
Time: Time,
|
|
1963
|
+
TimeInput: TimeInput,
|
|
1964
|
+
Tr: Tr,
|
|
1965
|
+
Track: Track,
|
|
1966
|
+
UnorderedList: UnorderedList,
|
|
1967
|
+
UrlInput: UrlInput,
|
|
1968
|
+
Var: Var,
|
|
1969
|
+
Video: Video,
|
|
1970
|
+
Wbr: Wbr,
|
|
1971
|
+
WeekInput: WeekInput,
|
|
1972
|
+
When: When
|
|
1973
|
+
});
|
|
1974
|
+
|
|
1975
|
+
const RouteParamPatterns = {
|
|
1976
|
+
|
|
1977
|
+
};
|
|
1978
|
+
|
|
1979
|
+
/**
|
|
1980
|
+
*
|
|
1981
|
+
* @param {string} $path
|
|
1982
|
+
* @param {Function} $component
|
|
1983
|
+
* @param {{name:?string, middlewares:Function[], shouldRebuild:Boolean, with: Object }}$options
|
|
1984
|
+
* @class
|
|
1985
|
+
*/
|
|
1986
|
+
function Route($path, $component, $options = {}) {
|
|
1987
|
+
|
|
1988
|
+
$path = '/'+trim($path, '/');
|
|
1989
|
+
|
|
1990
|
+
let $pattern = null;
|
|
1991
|
+
let $name = $options.name || null;
|
|
1992
|
+
|
|
1993
|
+
const $middlewares = $options.middlewares || [];
|
|
1994
|
+
const $shouldRebuild = $options.shouldRebuild || false;
|
|
1995
|
+
const $paramsValidators = $options.with || {};
|
|
1996
|
+
|
|
1997
|
+
const $params = {};
|
|
1998
|
+
const $paramsNames = [];
|
|
1999
|
+
|
|
2000
|
+
|
|
2001
|
+
const paramsExtractor = (description) => {
|
|
2002
|
+
if(!description) return null;
|
|
2003
|
+
const [name, type] = description.split(':');
|
|
2004
|
+
|
|
2005
|
+
let pattern = $paramsValidators[name];
|
|
2006
|
+
if(!pattern && type) {
|
|
2007
|
+
pattern = RouteParamPatterns[type];
|
|
2008
|
+
}
|
|
2009
|
+
if(!pattern) {
|
|
2010
|
+
pattern = '[^/]+';
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
pattern = pattern.replace('(', '(?:');
|
|
2014
|
+
|
|
2015
|
+
return { name, pattern: `(${pattern})` };
|
|
2016
|
+
};
|
|
2017
|
+
|
|
2018
|
+
const getPattern = () => {
|
|
2019
|
+
if($pattern) {
|
|
2020
|
+
return $pattern;
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
const patternDescription = $path.replace(/\{(.*?)}/ig, (block, definition) => {
|
|
2024
|
+
const description = paramsExtractor(definition);
|
|
2025
|
+
if(!description || !description.pattern) return block;
|
|
2026
|
+
$params[description.name] = description.pattern;
|
|
2027
|
+
$paramsNames.push(description.name);
|
|
2028
|
+
return description.pattern;
|
|
2029
|
+
});
|
|
2030
|
+
|
|
2031
|
+
$pattern = new RegExp('^'+patternDescription+'$');
|
|
2032
|
+
return $pattern;
|
|
2033
|
+
};
|
|
2034
|
+
|
|
2035
|
+
this.name = () => $name;
|
|
2036
|
+
this.component = () => $component;
|
|
2037
|
+
this.middlewares = () => $middlewares;
|
|
2038
|
+
this.shouldRebuild = () => $shouldRebuild;
|
|
2039
|
+
this.path = () => $path;
|
|
2040
|
+
|
|
2041
|
+
/**
|
|
2042
|
+
*
|
|
2043
|
+
* @param {string} path
|
|
2044
|
+
*/
|
|
2045
|
+
this.match = function(path) {
|
|
2046
|
+
path = '/'+trim(path, '/');
|
|
2047
|
+
const match = getPattern().exec(path);
|
|
2048
|
+
if(!match) return false;
|
|
2049
|
+
const params = {};
|
|
2050
|
+
|
|
2051
|
+
getPattern().exec(path).forEach((value, index) => {
|
|
2052
|
+
if(index < 1) return;
|
|
2053
|
+
const name = $paramsNames[index - 1];
|
|
2054
|
+
params[name] = value;
|
|
2055
|
+
});
|
|
2056
|
+
|
|
2057
|
+
return params;
|
|
2058
|
+
};
|
|
2059
|
+
/**
|
|
2060
|
+
* @param {{params: ?Object, query: ?Object, basePath: ?string}} configs
|
|
2061
|
+
*/
|
|
2062
|
+
this.url = function(configs) {
|
|
2063
|
+
const path = $path.replace(/\{(.*?)}/ig, (block, definition) => {
|
|
2064
|
+
const description = paramsExtractor(definition);
|
|
2065
|
+
if(configs.params && configs.params[description.name]) {
|
|
2066
|
+
return configs.params[description.name];
|
|
2067
|
+
}
|
|
2068
|
+
throw new Error(`Missing parameter '${description.name}'`);
|
|
2069
|
+
});
|
|
2070
|
+
|
|
2071
|
+
const queryString = (typeof configs.query === 'object') ? (new URLSearchParams(configs.query)).toString() : null;
|
|
2072
|
+
return (configs.basePath ? configs.basePath : '') + (queryString ? `${path}?${queryString}` : path);
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
class RouterError extends Error {
|
|
2077
|
+
constructor(message, context) {
|
|
2078
|
+
super(message);
|
|
2079
|
+
this.context = context;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
const RouteGroupHelper = {
|
|
2085
|
+
/**
|
|
2086
|
+
*
|
|
2087
|
+
* @param {{suffix: string, options: {middlewares: Function[], name: string}}[]} $groupTree
|
|
2088
|
+
* @param {string} path
|
|
2089
|
+
* @returns {string}
|
|
2090
|
+
*/
|
|
2091
|
+
fullPath: ($groupTree, path) => {
|
|
2092
|
+
const fullPath = [];
|
|
2093
|
+
$groupTree.forEach(group => {
|
|
2094
|
+
fullPath.push(trim(group.suffix, '/'));
|
|
2095
|
+
});
|
|
2096
|
+
fullPath.push(trim(path, '/'));
|
|
2097
|
+
return fullPath.join('/');
|
|
2098
|
+
},
|
|
2099
|
+
/**
|
|
2100
|
+
*
|
|
2101
|
+
* @param {{suffix: string, options: {middlewares: Function[], name: string}}[]} $groupTree
|
|
2102
|
+
* @param {Function[]} middlewares
|
|
2103
|
+
* @returns {Function[]}
|
|
2104
|
+
*/
|
|
2105
|
+
fullMiddlewares: ($groupTree, middlewares) => {
|
|
2106
|
+
const fullMiddlewares = [];
|
|
2107
|
+
$groupTree.forEach(group => {
|
|
2108
|
+
if(group.options.middlewares) {
|
|
2109
|
+
fullMiddlewares.push(...group.options.middlewares);
|
|
2110
|
+
}
|
|
2111
|
+
});
|
|
2112
|
+
if(middlewares) {
|
|
2113
|
+
fullMiddlewares.push(...middlewares);
|
|
2114
|
+
}
|
|
2115
|
+
return fullMiddlewares;
|
|
2116
|
+
},
|
|
2117
|
+
/**
|
|
2118
|
+
*
|
|
2119
|
+
* @param {{suffix: string, options: {middlewares: Function[], name: string}}[]} $groupTree
|
|
2120
|
+
* @param {string} name
|
|
2121
|
+
* @returns {string}
|
|
2122
|
+
*/
|
|
2123
|
+
fullName: ($groupTree, name) => {
|
|
2124
|
+
const fullName = [];
|
|
2125
|
+
$groupTree.forEach(group => {
|
|
2126
|
+
if(group.options?.name) {
|
|
2127
|
+
fullName.push(group.options.name);
|
|
2128
|
+
}
|
|
2129
|
+
});
|
|
2130
|
+
name && fullName.push(name);
|
|
2131
|
+
return fullName.join('.');
|
|
2132
|
+
}
|
|
2133
|
+
};
|
|
2134
|
+
|
|
2135
|
+
function HashRouter() {
|
|
2136
|
+
|
|
2137
|
+
const $history = [];
|
|
2138
|
+
let $currentIndex = 0;
|
|
2139
|
+
|
|
2140
|
+
/**
|
|
2141
|
+
*
|
|
2142
|
+
* @param {number} delta
|
|
2143
|
+
*/
|
|
2144
|
+
const go = (delta) => {
|
|
2145
|
+
const index = $currentIndex + delta;
|
|
2146
|
+
if(!$history[index]) {
|
|
2147
|
+
return;
|
|
2148
|
+
}
|
|
2149
|
+
$currentIndex = index;
|
|
2150
|
+
const { route, params, query, path } = $history[index];
|
|
2151
|
+
setHash(path);
|
|
2152
|
+
};
|
|
2153
|
+
|
|
2154
|
+
const canGoBack = function() {
|
|
2155
|
+
return $currentIndex > 0;
|
|
2156
|
+
};
|
|
2157
|
+
const canGoForward = function() {
|
|
2158
|
+
return $currentIndex < $history.length - 1;
|
|
2159
|
+
};
|
|
2160
|
+
|
|
2161
|
+
/**
|
|
2162
|
+
*
|
|
2163
|
+
* @param {string} path
|
|
2164
|
+
*/
|
|
2165
|
+
const setHash = (path) => {
|
|
2166
|
+
window.location.replace(`${window.location.pathname}${window.location.search}#${path}`);
|
|
2167
|
+
};
|
|
2168
|
+
|
|
2169
|
+
const getCurrentHash = () => window.location.hash.slice(1);
|
|
2170
|
+
|
|
2171
|
+
/**
|
|
2172
|
+
* @param {string|{name:string,params?:Object, query?:Object }} target
|
|
2173
|
+
*/
|
|
2174
|
+
this.push = function(target) {
|
|
2175
|
+
const { route, params, query, path } = this.resolve(target);
|
|
2176
|
+
if(path === getCurrentHash()) {
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
$history.splice($currentIndex + 1);
|
|
2180
|
+
$history.push({ route, params, query, path });
|
|
2181
|
+
$currentIndex++;
|
|
2182
|
+
setHash(path);
|
|
2183
|
+
};
|
|
2184
|
+
/**
|
|
2185
|
+
*
|
|
2186
|
+
* @param {string|{name:string,params?:Object, query?:Object }} target
|
|
2187
|
+
*/
|
|
2188
|
+
this.replace = function(target) {
|
|
2189
|
+
const { route, params, query, path } = this.resolve(target);
|
|
2190
|
+
if(path === getCurrentHash()) {
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
$history[$currentIndex] = { route, params, query, path };
|
|
2194
|
+
};
|
|
2195
|
+
this.forward = function() {
|
|
2196
|
+
return canGoForward() && go(1);
|
|
2197
|
+
};
|
|
2198
|
+
this.back = function() {
|
|
2199
|
+
return canGoBack() && go(-1);
|
|
2200
|
+
};
|
|
2201
|
+
|
|
2202
|
+
/**
|
|
2203
|
+
* @param {string} defaultPath
|
|
2204
|
+
*/
|
|
2205
|
+
this.init = function(defaultPath) {
|
|
2206
|
+
window.addEventListener('hashchange', () => {
|
|
2207
|
+
const { route, params, query, path } = this.resolve(getCurrentHash());
|
|
2208
|
+
this.handleRouteChange(route, params, query, path);
|
|
2209
|
+
});
|
|
2210
|
+
const { route, params, query, path } = this.resolve(defaultPath || getCurrentHash());
|
|
2211
|
+
$history.push({ route, params, query, path });
|
|
2212
|
+
$currentIndex = 0;
|
|
2213
|
+
this.handleRouteChange(route, params, query, path);
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
function HistoryRouter() {
|
|
2218
|
+
|
|
2219
|
+
/**
|
|
2220
|
+
*
|
|
2221
|
+
* @param {string|{name:string,params?:Object, query?:Object }} target
|
|
2222
|
+
*/
|
|
2223
|
+
this.push = function(target) {
|
|
2224
|
+
try {
|
|
2225
|
+
const { route, path, params, query } = this.resolve(target);
|
|
2226
|
+
if(window.history.state && window.history.state.path === path) {
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
window.history.pushState({ name: route.name(), params, path}, route.name() || path , path);
|
|
2230
|
+
this.handleRouteChange(route, params, query, path);
|
|
2231
|
+
} catch (e) {
|
|
2232
|
+
DebugManager.error('HistoryRouter', 'Error in pushState', e);
|
|
2233
|
+
}
|
|
2234
|
+
};
|
|
2235
|
+
/**
|
|
2236
|
+
*
|
|
2237
|
+
* @param {string|{name:string,params?:Object, query?:Object }} target
|
|
2238
|
+
*/
|
|
2239
|
+
this.replace = function(target) {
|
|
2240
|
+
const { route, path, params } = this.resolve(target);
|
|
2241
|
+
try {
|
|
2242
|
+
window.history.replaceState({ name: route.name(), params, path}, route.name() || path , path);
|
|
2243
|
+
this.handleRouteChange(route, params, {}, path);
|
|
2244
|
+
} catch(e) {
|
|
2245
|
+
DebugManager.error('HistoryRouter', 'Error in replaceState', e);
|
|
2246
|
+
}
|
|
2247
|
+
};
|
|
2248
|
+
this.forward = function() {
|
|
2249
|
+
window.history.forward();
|
|
2250
|
+
};
|
|
2251
|
+
|
|
2252
|
+
this.back = function() {
|
|
2253
|
+
window.history.back();
|
|
2254
|
+
};
|
|
2255
|
+
|
|
2256
|
+
/**
|
|
2257
|
+
* @param {string} defaultPath
|
|
2258
|
+
*/
|
|
2259
|
+
this.init = function(defaultPath) {
|
|
2260
|
+
window.addEventListener('popstate', (event) => {
|
|
2261
|
+
try {
|
|
2262
|
+
if(!event.state || !event.state.path) {
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2265
|
+
const statePath = event.state.path;
|
|
2266
|
+
const {route, params, query, path} = this.resolve(statePath);
|
|
2267
|
+
if(!route) {
|
|
2268
|
+
return;
|
|
2269
|
+
}
|
|
2270
|
+
this.handleRouteChange(route, params, query, path);
|
|
2271
|
+
} catch(e) {
|
|
2272
|
+
DebugManager.error('HistoryRouter', 'Error in popstate event', e);
|
|
2273
|
+
}
|
|
2274
|
+
});
|
|
2275
|
+
const { route, params, query, path } = this.resolve(defaultPath || (window.location.pathname+window.location.search));
|
|
2276
|
+
this.handleRouteChange(route, params, query, path);
|
|
2277
|
+
};
|
|
2278
|
+
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
function MemoryRouter() {
|
|
2282
|
+
const $history = [];
|
|
2283
|
+
let $currentIndex = 0;
|
|
2284
|
+
|
|
2285
|
+
/**
|
|
2286
|
+
*
|
|
2287
|
+
* @param {number} delta
|
|
2288
|
+
*/
|
|
2289
|
+
const go = (delta) => {
|
|
2290
|
+
const index = $currentIndex + delta;
|
|
2291
|
+
if(!$history[index]) {
|
|
2292
|
+
return;
|
|
2293
|
+
}
|
|
2294
|
+
$currentIndex = index;
|
|
2295
|
+
const { route, params, query, path } = $history[index];
|
|
2296
|
+
this.handleRouteChange(route, params, query, path);
|
|
2297
|
+
};
|
|
2298
|
+
|
|
2299
|
+
const canGoBack = function() {
|
|
2300
|
+
return $currentIndex > 0;
|
|
2301
|
+
};
|
|
2302
|
+
const canGoForward = function() {
|
|
2303
|
+
return $currentIndex < $history.length - 1;
|
|
2304
|
+
};
|
|
2305
|
+
|
|
2306
|
+
/**
|
|
2307
|
+
*
|
|
2308
|
+
* @param {string|{name:string,params?:Object, query?:Object }} target
|
|
2309
|
+
*/
|
|
2310
|
+
this.push = function(target) {
|
|
2311
|
+
const { route, params, query, path} = this.resolve(target);
|
|
2312
|
+
if($history[$currentIndex] && $history[$currentIndex].path === path) {
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2315
|
+
$history.splice($currentIndex + 1);
|
|
2316
|
+
$history.push({ route, params, query, path });
|
|
2317
|
+
$currentIndex++;
|
|
2318
|
+
this.handleRouteChange(route, params, query, path);
|
|
2319
|
+
};
|
|
2320
|
+
|
|
2321
|
+
/**
|
|
2322
|
+
*
|
|
2323
|
+
* @param {string|{name:string,params?:Object, query?:Object }} target
|
|
2324
|
+
*/
|
|
2325
|
+
this.replace = function(target) {
|
|
2326
|
+
const { route, params, query, path} = this.resolve(target);
|
|
2327
|
+
$history[$currentIndex] = { route, params, query, path };
|
|
2328
|
+
this.handleRouteChange(route, params, query, path);
|
|
2329
|
+
};
|
|
2330
|
+
|
|
2331
|
+
this.forward = function() {
|
|
2332
|
+
return canGoForward() && go(1);
|
|
2333
|
+
};
|
|
2334
|
+
|
|
2335
|
+
this.back = function() {
|
|
2336
|
+
return canGoBack() && go(-1);
|
|
2337
|
+
};
|
|
2338
|
+
|
|
2339
|
+
/**
|
|
2340
|
+
* @param {string} defaultPath
|
|
2341
|
+
*/
|
|
2342
|
+
this.init = function(defaultPath) {
|
|
2343
|
+
const currentPath = defaultPath || (window.location.pathname + window.location.search);
|
|
2344
|
+
const { route, params, query, path } = this.resolve(currentPath);
|
|
2345
|
+
$history.push({ route, params, query, path });
|
|
2346
|
+
$currentIndex = 0;
|
|
2347
|
+
|
|
2348
|
+
this.handleRouteChange(route, params, query, path);
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
/**
|
|
2353
|
+
*
|
|
2354
|
+
* @param {Router} router
|
|
2355
|
+
* @param {?HTMLElement} container
|
|
2356
|
+
*/
|
|
2357
|
+
function RouterComponent(router, container) {
|
|
2358
|
+
|
|
2359
|
+
const $cache = new Map();
|
|
2360
|
+
|
|
2361
|
+
const updateContainer = function(node) {
|
|
2362
|
+
container.innerHTML = '';
|
|
2363
|
+
container.appendChild(node);
|
|
2364
|
+
};
|
|
2365
|
+
|
|
2366
|
+
const handleCurrentRouterState = function(state) {
|
|
2367
|
+
if(!state.route) {
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
const { route, params, query, path } = state;
|
|
2371
|
+
if($cache.has(path)) {
|
|
2372
|
+
const cacheNode = $cache.get(path);
|
|
2373
|
+
updateContainer(cacheNode);
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2376
|
+
const Component = route.component();
|
|
2377
|
+
const node = Component({ params, query });
|
|
2378
|
+
$cache.set(path, node);
|
|
2379
|
+
updateContainer(node);
|
|
2380
|
+
};
|
|
2381
|
+
|
|
2382
|
+
router.subscribe(handleCurrentRouterState);
|
|
2383
|
+
|
|
2384
|
+
handleCurrentRouterState(router.currentState());
|
|
2385
|
+
return container;
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
const DEFAULT_ROUTER_NAME = 'default';
|
|
2389
|
+
|
|
2390
|
+
/**
|
|
2391
|
+
*
|
|
2392
|
+
* @param {{mode: 'memory'|'history'|'hash'}} $options
|
|
2393
|
+
* @constructor
|
|
2394
|
+
*/
|
|
2395
|
+
function Router($options = {}) {
|
|
2396
|
+
|
|
2397
|
+
/** @type {Route[]} */
|
|
2398
|
+
const $routes = [];
|
|
2399
|
+
/** @type {{[string]: Route}} */
|
|
2400
|
+
const $routesByName = {};
|
|
2401
|
+
const $groupTree = [];
|
|
2402
|
+
const $listeners = [];
|
|
2403
|
+
const $currentState = { route: null, params: null, query: null, path: null, hash: null };
|
|
2404
|
+
|
|
2405
|
+
if($options.mode === 'hash') {
|
|
2406
|
+
HashRouter.apply(this, []);
|
|
2407
|
+
} else if($options.mode === 'history') {
|
|
2408
|
+
HistoryRouter.apply(this, []);
|
|
2409
|
+
} else if($options.mode === 'memory') {
|
|
2410
|
+
MemoryRouter.apply(this, []);
|
|
2411
|
+
} else {
|
|
2412
|
+
throw new RouterError('Invalid router mode '+$options.mode);
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
const trigger = function(request, next) {
|
|
2416
|
+
for(const listener of $listeners) {
|
|
2417
|
+
try {
|
|
2418
|
+
listener(request);
|
|
2419
|
+
next && next(request);
|
|
2420
|
+
} catch (e) {
|
|
2421
|
+
DebugManager.warn('Route Listener', 'Error in listener:', e);
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
};
|
|
2425
|
+
|
|
2426
|
+
this.routes = () => [...$routes];
|
|
2427
|
+
this.currentState = () => ({ ...$currentState });
|
|
2428
|
+
|
|
2429
|
+
/**
|
|
2430
|
+
*
|
|
2431
|
+
* @param {string} path
|
|
2432
|
+
* @param {Function} component
|
|
2433
|
+
* @param {{name:?string, middlewares:Function[], shouldRebuild:Boolean, with: Object }} options
|
|
2434
|
+
* @returns {this}
|
|
2435
|
+
*/
|
|
2436
|
+
this.add = function(path, component, options) {
|
|
2437
|
+
const route = new Route(RouteGroupHelper.fullPath($groupTree, path), component, {
|
|
2438
|
+
...options,
|
|
2439
|
+
middlewares: RouteGroupHelper.fullMiddlewares($groupTree, options?.middlewares || []),
|
|
2440
|
+
name: options?.name ? RouteGroupHelper.fullName($groupTree, options.name) : null,
|
|
2441
|
+
});
|
|
2442
|
+
$routes.push(route);
|
|
2443
|
+
if(route.name()) {
|
|
2444
|
+
$routesByName[route.name()] = route;
|
|
2445
|
+
}
|
|
2446
|
+
return this;
|
|
2447
|
+
};
|
|
2448
|
+
|
|
2449
|
+
/**
|
|
2450
|
+
*
|
|
2451
|
+
* @param {string} suffix
|
|
2452
|
+
* @param {{ middlewares: Function[], name: string}} options
|
|
2453
|
+
* @param {Function} callback
|
|
2454
|
+
* @returns {this}
|
|
2455
|
+
*/
|
|
2456
|
+
this.group = function(suffix, options, callback) {
|
|
2457
|
+
if(!Validator.isFunction(callback)) {
|
|
2458
|
+
throw new RouterError('Callback must be a function');
|
|
2459
|
+
}
|
|
2460
|
+
$groupTree.push({suffix, options});
|
|
2461
|
+
callback();
|
|
2462
|
+
$groupTree.pop();
|
|
2463
|
+
return this;
|
|
2464
|
+
};
|
|
2465
|
+
|
|
2466
|
+
/**
|
|
2467
|
+
*
|
|
2468
|
+
* @param {string} name
|
|
2469
|
+
* @param {Object}params
|
|
2470
|
+
* @param {Object} query
|
|
2471
|
+
* @returns {*}
|
|
2472
|
+
*/
|
|
2473
|
+
this.generateUrl = function(name, params = {}, query = {}) {
|
|
2474
|
+
const route = $routesByName[name];
|
|
2475
|
+
if(!route) {
|
|
2476
|
+
throw new RouterError(`Route not found for name: ${name}`);
|
|
2477
|
+
}
|
|
2478
|
+
return route.url({ params, query });
|
|
2479
|
+
};
|
|
2480
|
+
|
|
2481
|
+
/**
|
|
2482
|
+
*
|
|
2483
|
+
* @param {string|{name:string,params?:Object, query?:Object }} target
|
|
2484
|
+
* @returns {{route:Route, params:Object, query:Object, path:string}}
|
|
2485
|
+
*/
|
|
2486
|
+
this.resolve = function(target) {
|
|
2487
|
+
if(Validator.isJson(target)) {
|
|
2488
|
+
const route = $routesByName[target.name];
|
|
2489
|
+
if(!route) {
|
|
2490
|
+
throw new RouterError(`Route not found for name: ${target.name}`);
|
|
2491
|
+
}
|
|
2492
|
+
return {
|
|
2493
|
+
route,
|
|
2494
|
+
params: target.params,
|
|
2495
|
+
query: target.query,
|
|
2496
|
+
path: route.url({ ...target })
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
const [urlPath, urlQuery] = target.split('?');
|
|
2501
|
+
const path = '/'+trim(urlPath, '/');
|
|
2502
|
+
let routeFound = null, params;
|
|
2503
|
+
|
|
2504
|
+
for(const route of $routes) {
|
|
2505
|
+
params = route.match(path);
|
|
2506
|
+
if(params) {
|
|
2507
|
+
routeFound = route;
|
|
2508
|
+
break;
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
if(!routeFound) {
|
|
2512
|
+
throw new RouterError(`Route not found for url: ${urlPath}`);
|
|
2513
|
+
}
|
|
2514
|
+
const queryParams = {};
|
|
2515
|
+
if(urlQuery) {
|
|
2516
|
+
const queries = new URLSearchParams(urlQuery).entries();
|
|
2517
|
+
for (const [key, value] of queries) {
|
|
2518
|
+
queryParams[key] = value;
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
return { route: routeFound, params, query: queryParams, path: target };
|
|
2523
|
+
};
|
|
2524
|
+
|
|
2525
|
+
/**
|
|
2526
|
+
*
|
|
2527
|
+
* @param {Function} listener
|
|
2528
|
+
* @returns {(function(): void)|*}
|
|
2529
|
+
*/
|
|
2530
|
+
this.subscribe = function(listener) {
|
|
2531
|
+
if(!Validator.isFunction(listener)) {
|
|
2532
|
+
throw new RouterError('Listener must be a function');
|
|
2533
|
+
}
|
|
2534
|
+
$listeners.push(listener);
|
|
2535
|
+
return () => {
|
|
2536
|
+
$listeners.splice($listeners.indexOf(listener), 1);
|
|
2537
|
+
};
|
|
2538
|
+
};
|
|
2539
|
+
|
|
2540
|
+
/**
|
|
2541
|
+
*
|
|
2542
|
+
* @param {Route} route
|
|
2543
|
+
* @param {Object} params
|
|
2544
|
+
* @param {Object} query
|
|
2545
|
+
* @param {string} path
|
|
2546
|
+
*/
|
|
2547
|
+
this.handleRouteChange = function(route, params, query, path) {
|
|
2548
|
+
$currentState.route = route;
|
|
2549
|
+
$currentState.params = params;
|
|
2550
|
+
$currentState.query = query;
|
|
2551
|
+
$currentState.path = path;
|
|
2552
|
+
|
|
2553
|
+
const middlewares = [...route.middlewares(), trigger];
|
|
2554
|
+
let currentIndex = 0;
|
|
2555
|
+
const request = { ...$currentState };
|
|
2556
|
+
|
|
2557
|
+
const next = (editableRequest) => {
|
|
2558
|
+
currentIndex++;
|
|
2559
|
+
if(currentIndex >= middlewares.length) {
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
return middlewares[currentIndex](editableRequest || request, next);
|
|
2563
|
+
};
|
|
2564
|
+
return middlewares[currentIndex](request, next);
|
|
2565
|
+
};
|
|
2566
|
+
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
Router.routers = {};
|
|
2570
|
+
|
|
2571
|
+
/**
|
|
2572
|
+
*
|
|
2573
|
+
* @param {{mode: 'memory'|'history'|'hash', name?:string, entry?: string}} options
|
|
2574
|
+
* @param {Function} callback
|
|
2575
|
+
* @param {Element} container
|
|
2576
|
+
*/
|
|
2577
|
+
Router.create = function(options, callback) {
|
|
2578
|
+
if(!Validator.isFunction(callback)) {
|
|
2579
|
+
DebugManager.error('Router', 'Callback must be a function', e);
|
|
2580
|
+
throw new RouterError('Callback must be a function');
|
|
2581
|
+
}
|
|
2582
|
+
const router = new Router(options);
|
|
2583
|
+
Router.routers[options.name || DEFAULT_ROUTER_NAME] = router;
|
|
2584
|
+
callback(router);
|
|
2585
|
+
|
|
2586
|
+
router.init(options.entry);
|
|
2587
|
+
|
|
2588
|
+
router.mount = function(container) {
|
|
2589
|
+
if(Validator.isString(container)) {
|
|
2590
|
+
const mountContainer = document.querySelector(container);
|
|
2591
|
+
if(!mountContainer) {
|
|
2592
|
+
throw new RouterError(`Container not found for selector: ${container}`);
|
|
2593
|
+
}
|
|
2594
|
+
container = mountContainer;
|
|
2595
|
+
} else if(!Validator.isElement(container)) {
|
|
2596
|
+
throw new RouterError('Container must be a string or an Element');
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
return RouterComponent(router, container);
|
|
2600
|
+
};
|
|
2601
|
+
|
|
2602
|
+
return router;
|
|
2603
|
+
};
|
|
2604
|
+
|
|
2605
|
+
Router.get = function(name) {
|
|
2606
|
+
const router = Router.routers[name || DEFAULT_ROUTER_NAME];
|
|
2607
|
+
if(!router) {
|
|
2608
|
+
throw new RouterError(`Router not found for name: ${name}`);
|
|
2609
|
+
}
|
|
2610
|
+
return router;
|
|
2611
|
+
};
|
|
2612
|
+
|
|
2613
|
+
Router.push = function(target, name = null) {
|
|
2614
|
+
return Router.get(name).push(target);
|
|
2615
|
+
};
|
|
2616
|
+
|
|
2617
|
+
Router.replace = function(target, name = null) {
|
|
2618
|
+
return Router.get(name).replace(target);
|
|
2619
|
+
};
|
|
2620
|
+
|
|
2621
|
+
Router.forward = function(name = null) {
|
|
2622
|
+
return Router.get(name).forward();
|
|
2623
|
+
};
|
|
2624
|
+
Router.back = function(name = null) {
|
|
2625
|
+
return Router.get(name).back();
|
|
2626
|
+
};
|
|
2627
|
+
|
|
2628
|
+
function Link(options, children){
|
|
2629
|
+
const { to, href, ...attributes } = options;
|
|
2630
|
+
const target = to || href;
|
|
2631
|
+
if(Validator.isString(target)) {
|
|
2632
|
+
const router = Router.get();
|
|
2633
|
+
return Link$1({ ...attributes, href: target}, children).nd.on.prevent.click(() => {
|
|
2634
|
+
router.push(target);
|
|
2635
|
+
});
|
|
2636
|
+
}
|
|
2637
|
+
const routerName = target.router || DEFAULT_ROUTER_NAME;
|
|
2638
|
+
const router = Router.get(routerName);
|
|
2639
|
+
console.log(routerName);
|
|
2640
|
+
if(!router) {
|
|
2641
|
+
throw new RouterError('Router not found "'+routerName+'" for link "'+target.name+'"');
|
|
2642
|
+
}
|
|
2643
|
+
const url = router.generateUrl(target.name, target.params, target.query);
|
|
2644
|
+
return Link$1({ ...attributes, href: url }, children).nd.on.prevent.click(() => {
|
|
2645
|
+
router.push(url);
|
|
2646
|
+
});
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
Link.blank = function(attributes, children){
|
|
2650
|
+
return Link$1({ ...attributes, target: '_blank'}, children);
|
|
2651
|
+
};
|
|
2652
|
+
|
|
2653
|
+
var router = /*#__PURE__*/Object.freeze({
|
|
2654
|
+
__proto__: null,
|
|
2655
|
+
Link: Link,
|
|
2656
|
+
RouteParamPatterns: RouteParamPatterns,
|
|
2657
|
+
Router: Router
|
|
2658
|
+
});
|
|
2659
|
+
|
|
2660
|
+
exports.ArgTypes = ArgTypes;
|
|
2661
|
+
exports.ElementCreator = ElementCreator;
|
|
2662
|
+
exports.HtmlElementWrapper = HtmlElementWrapper;
|
|
2663
|
+
exports.Observable = Observable;
|
|
2664
|
+
exports.Store = Store;
|
|
2665
|
+
exports.elements = elements;
|
|
2666
|
+
exports.router = router;
|
|
2667
|
+
exports.withValidation = withValidation;
|
|
2668
|
+
|
|
2669
|
+
return exports;
|
|
2670
|
+
|
|
2671
|
+
})({});
|