native-document 1.0.0
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 +2346 -0
- package/dist/native-document.min.js +1 -0
- package/elements.js +2 -0
- package/index.js +11 -0
- package/package.json +16 -0
- package/readme.md +495 -0
- package/rollup.config.js +29 -0
- package/router.js +9 -0
- package/src/data/MemoryManager.js +60 -0
- package/src/data/Observable.js +162 -0
- package/src/data/ObservableChecker.js +24 -0
- package/src/data/ObservableItem.js +101 -0
- package/src/data/Store.js +74 -0
- package/src/elements/content-formatter.js +32 -0
- package/src/elements/control/for-each.js +110 -0
- package/src/elements/control/show-if.js +86 -0
- package/src/elements/control/switch.js +88 -0
- package/src/elements/description-list.js +5 -0
- package/src/elements/form.js +71 -0
- package/src/elements/html5-semantics.js +12 -0
- package/src/elements/img.js +45 -0
- package/src/elements/index.js +21 -0
- package/src/elements/interactive.js +7 -0
- package/src/elements/list.js +6 -0
- package/src/elements/medias.js +8 -0
- package/src/elements/meta-data.js +9 -0
- package/src/elements/table.js +14 -0
- package/src/errors/ArgTypesError.js +7 -0
- package/src/errors/NativeDocumentError.js +8 -0
- package/src/errors/RouterError.js +9 -0
- package/src/router/Route.js +102 -0
- package/src/router/RouteGroupHelper.js +52 -0
- package/src/router/Router.js +232 -0
- package/src/router/RouterComponent.js +37 -0
- package/src/router/link.js +27 -0
- package/src/router/modes/HashRouter.js +83 -0
- package/src/router/modes/HistoryRouter.js +66 -0
- package/src/router/modes/MemoryRouter.js +71 -0
- package/src/utils/args-types.js +100 -0
- package/src/utils/debug-manager.js +34 -0
- package/src/utils/helpers.js +37 -0
- package/src/utils/prototypes.js +16 -0
- package/src/utils/validator.js +96 -0
- package/src/wrappers/AttributesWrapper.js +94 -0
- package/src/wrappers/DocumentObserver.js +51 -0
- package/src/wrappers/HtmlElementEventsWrapper.js +77 -0
- package/src/wrappers/HtmlElementWrapper.js +174 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
export default function HashRouter() {
|
|
4
|
+
|
|
5
|
+
const $history = [];
|
|
6
|
+
let $currentIndex = 0;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
*
|
|
10
|
+
* @param {number} delta
|
|
11
|
+
*/
|
|
12
|
+
const go = (delta) => {
|
|
13
|
+
const index = $currentIndex + delta;
|
|
14
|
+
if(!$history[index]) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
$currentIndex = index;
|
|
18
|
+
const { route, params, query, path } = $history[index];
|
|
19
|
+
setHash(path);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const canGoBack = function() {
|
|
23
|
+
return $currentIndex > 0;
|
|
24
|
+
};
|
|
25
|
+
const canGoForward = function() {
|
|
26
|
+
return $currentIndex < $history.length - 1;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
*
|
|
31
|
+
* @param {string} path
|
|
32
|
+
*/
|
|
33
|
+
const setHash = (path) => {
|
|
34
|
+
window.location.replace(`${window.location.pathname}${window.location.search}#${path}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const getCurrentHash = () => window.location.hash.slice(1);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {string|{name:string,params?:Object, query?:Object }} target
|
|
41
|
+
*/
|
|
42
|
+
this.push = function(target) {
|
|
43
|
+
const { route, params, query, path } = this.resolve(target);
|
|
44
|
+
if(path === getCurrentHash()) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
$history.splice($currentIndex + 1);
|
|
48
|
+
$history.push({ route, params, query, path });
|
|
49
|
+
$currentIndex++;
|
|
50
|
+
setHash(path);
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
*
|
|
54
|
+
* @param {string|{name:string,params?:Object, query?:Object }} target
|
|
55
|
+
*/
|
|
56
|
+
this.replace = function(target) {
|
|
57
|
+
const { route, params, query, path } = this.resolve(target);
|
|
58
|
+
if(path === getCurrentHash()) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
$history[$currentIndex] = { route, params, query, path };
|
|
62
|
+
};
|
|
63
|
+
this.forward = function() {
|
|
64
|
+
return canGoForward() && go(1);
|
|
65
|
+
};
|
|
66
|
+
this.back = function() {
|
|
67
|
+
return canGoBack() && go(-1);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {string} defaultPath
|
|
72
|
+
*/
|
|
73
|
+
this.init = function(defaultPath) {
|
|
74
|
+
window.addEventListener('hashchange', () => {
|
|
75
|
+
const { route, params, query, path } = this.resolve(getCurrentHash());
|
|
76
|
+
this.handleRouteChange(route, params, query, path);
|
|
77
|
+
});
|
|
78
|
+
const { route, params, query, path } = this.resolve(defaultPath || getCurrentHash());
|
|
79
|
+
$history.push({ route, params, query, path });
|
|
80
|
+
$currentIndex = 0;
|
|
81
|
+
this.handleRouteChange(route, params, query, path);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import RouterError from '../../errors/RouterError';
|
|
2
|
+
import DebugManager from "../../utils/debug-manager.js";
|
|
3
|
+
|
|
4
|
+
export default function HistoryRouter() {
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
*
|
|
8
|
+
* @param {string|{name:string,params?:Object, query?:Object }} target
|
|
9
|
+
*/
|
|
10
|
+
this.push = function(target) {
|
|
11
|
+
try {
|
|
12
|
+
const { route, path, params, query } = this.resolve(target);
|
|
13
|
+
if(window.history.state && window.history.state.path === path) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
window.history.pushState({ name: route.name(), params, path}, route.name() || path , path);
|
|
17
|
+
this.handleRouteChange(route, params, query, path);
|
|
18
|
+
} catch (e) {
|
|
19
|
+
DebugManager.error('HistoryRouter', 'Error in pushState', e);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
*
|
|
24
|
+
* @param {string|{name:string,params?:Object, query?:Object }} target
|
|
25
|
+
*/
|
|
26
|
+
this.replace = function(target) {
|
|
27
|
+
const { route, path, params } = this.resolve(target);
|
|
28
|
+
try {
|
|
29
|
+
window.history.replaceState({ name: route.name(), params, path}, route.name() || path , path);
|
|
30
|
+
this.handleRouteChange(route, params, {}, path);
|
|
31
|
+
} catch(e) {
|
|
32
|
+
DebugManager.error('HistoryRouter', 'Error in replaceState', e);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
this.forward = function() {
|
|
36
|
+
window.history.forward();
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
this.back = function() {
|
|
40
|
+
window.history.back();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {string} defaultPath
|
|
45
|
+
*/
|
|
46
|
+
this.init = function(defaultPath) {
|
|
47
|
+
window.addEventListener('popstate', (event) => {
|
|
48
|
+
try {
|
|
49
|
+
if(!event.state || !event.state.path) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const statePath = event.state.path;
|
|
53
|
+
const {route, params, query, path} = this.resolve(statePath);
|
|
54
|
+
if(!route) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
this.handleRouteChange(route, params, query, path);
|
|
58
|
+
} catch(e) {
|
|
59
|
+
DebugManager.error('HistoryRouter', 'Error in popstate event', e);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
const { route, params, query, path } = this.resolve(defaultPath || (window.location.pathname+window.location.search));
|
|
63
|
+
this.handleRouteChange(route, params, query, path);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
|
|
2
|
+
export default function MemoryRouter() {
|
|
3
|
+
const $history = [];
|
|
4
|
+
let $currentIndex = 0;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
*
|
|
8
|
+
* @param {number} delta
|
|
9
|
+
*/
|
|
10
|
+
const go = (delta) => {
|
|
11
|
+
const index = $currentIndex + delta;
|
|
12
|
+
if(!$history[index]) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
$currentIndex = index;
|
|
16
|
+
const { route, params, query, path } = $history[index];
|
|
17
|
+
this.handleRouteChange(route, params, query, path);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const canGoBack = function() {
|
|
21
|
+
return $currentIndex > 0;
|
|
22
|
+
};
|
|
23
|
+
const canGoForward = function() {
|
|
24
|
+
return $currentIndex < $history.length - 1;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
*
|
|
29
|
+
* @param {string|{name:string,params?:Object, query?:Object }} target
|
|
30
|
+
*/
|
|
31
|
+
this.push = function(target) {
|
|
32
|
+
const { route, params, query, path} = this.resolve(target);
|
|
33
|
+
if($history[$currentIndex] && $history[$currentIndex].path === path) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
$history.splice($currentIndex + 1);
|
|
37
|
+
$history.push({ route, params, query, path });
|
|
38
|
+
$currentIndex++;
|
|
39
|
+
this.handleRouteChange(route, params, query, path);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
*
|
|
44
|
+
* @param {string|{name:string,params?:Object, query?:Object }} target
|
|
45
|
+
*/
|
|
46
|
+
this.replace = function(target) {
|
|
47
|
+
const { route, params, query, path} = this.resolve(target);
|
|
48
|
+
$history[$currentIndex] = { route, params, query, path };
|
|
49
|
+
this.handleRouteChange(route, params, query, path);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
this.forward = function() {
|
|
53
|
+
return canGoForward() && go(1);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
this.back = function() {
|
|
57
|
+
return canGoBack() && go(-1);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {string} defaultPath
|
|
62
|
+
*/
|
|
63
|
+
this.init = function(defaultPath) {
|
|
64
|
+
const currentPath = defaultPath || (window.location.pathname + window.location.search);
|
|
65
|
+
const { route, params, query, path } = this.resolve(currentPath);
|
|
66
|
+
$history.push({ route, params, query, path });
|
|
67
|
+
$currentIndex = 0;
|
|
68
|
+
|
|
69
|
+
this.handleRouteChange(route, params, query, path);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import Validator from "./validator";
|
|
2
|
+
import ArgTypesError from "../errors/ArgTypesError";
|
|
3
|
+
import NativeDocumentError from "../errors/NativeDocumentError";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
*
|
|
7
|
+
* @type {{string: (function(*): {name: *, type: string, validate: function(*): boolean}),
|
|
8
|
+
* number: (function(*): {name: *, type: string, validate: function(*): boolean}),
|
|
9
|
+
* boolean: (function(*): {name: *, type: string, validate: function(*): boolean}),
|
|
10
|
+
* observable: (function(*): {name: *, type: string, validate: function(*): boolean}),
|
|
11
|
+
* element: (function(*): {name: *, type: string, validate: function(*): *}),
|
|
12
|
+
* function: (function(*): {name: *, type: string, validate: function(*): boolean}),
|
|
13
|
+
* object: (function(*): {name: *, type: string, validate: function(*): boolean}),
|
|
14
|
+
* objectNotNull: (function(*): {name: *, type: string, validate: function(*): *}),
|
|
15
|
+
* children: (function(*): {name: *, type: string, validate: function(*): *}),
|
|
16
|
+
* attributes: (function(*): {name: *, type: string, validate: function(*): *}),
|
|
17
|
+
* optional: (function(*): *&{optional: boolean}),
|
|
18
|
+
* oneOf: (function(*, ...[*]): {name: *, type: string, types: *[],
|
|
19
|
+
* validate: function(*): boolean})
|
|
20
|
+
* }}
|
|
21
|
+
*/
|
|
22
|
+
export const ArgTypes = {
|
|
23
|
+
string: (name) => ({ name, type: 'string', validate: (v) => Validator.isString(v) }),
|
|
24
|
+
number: (name) => ({ name, type: 'number', validate: (v) => Validator.isNumber(v) }),
|
|
25
|
+
boolean: (name) => ({ name, type: 'boolean', validate: (v) => Validator.isBoolean(v) }),
|
|
26
|
+
observable: (name) => ({ name, type: 'observable', validate: (v) => Validator.isObservable(v) }),
|
|
27
|
+
element: (name) => ({ name, type: 'element', validate: (v) => Validator.isElement(v) }),
|
|
28
|
+
function: (name) => ({ name, type: 'function', validate: (v) => Validator.isFunction(v) }),
|
|
29
|
+
object: (name) => ({ name, type: 'object', validate: (v) => (Validator.isObject(v)) }),
|
|
30
|
+
objectNotNull: (name) => ({ name, type: 'object', validate: (v) => (Validator.isObject(v) && v !== null) }),
|
|
31
|
+
children: (name) => ({ name, type: 'children', validate: (v) => Validator.validateChildren(v) }),
|
|
32
|
+
attributes: (name) => ({ name, type: 'attributes', validate: (v) => Validator.validateAttributes(v) }),
|
|
33
|
+
|
|
34
|
+
// Optional arguments
|
|
35
|
+
optional: (argType) => ({ ...argType, optional: true }),
|
|
36
|
+
|
|
37
|
+
// Union types
|
|
38
|
+
oneOf: (name, ...argTypes) => ({
|
|
39
|
+
name,
|
|
40
|
+
type: 'oneOf',
|
|
41
|
+
types: argTypes,
|
|
42
|
+
validate: (v) => argTypes.some(type => type.validate(v))
|
|
43
|
+
})
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
*
|
|
48
|
+
* @param {Array} args
|
|
49
|
+
* @param {Array} argSchema
|
|
50
|
+
* @param {string} fnName
|
|
51
|
+
*/
|
|
52
|
+
const validateArgs = (args, argSchema, fnName = 'Function') => {
|
|
53
|
+
if (!argSchema) return;
|
|
54
|
+
|
|
55
|
+
const errors = [];
|
|
56
|
+
|
|
57
|
+
// Check the number of arguments
|
|
58
|
+
const requiredCount = argSchema.filter(arg => !arg.optional).length;
|
|
59
|
+
if (args.length < requiredCount) {
|
|
60
|
+
errors.push(`${fnName}: Expected at least ${requiredCount} arguments, got ${args.length}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Validate each argument
|
|
64
|
+
argSchema.forEach((schema, index) => {
|
|
65
|
+
const position = index + 1;
|
|
66
|
+
const value = args[index];
|
|
67
|
+
|
|
68
|
+
if (value === undefined) {
|
|
69
|
+
if (!schema.optional) {
|
|
70
|
+
errors.push(`${fnName}: Missing required argument '${schema.name}' at position ${position}`);
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!schema.validate(value)) {
|
|
76
|
+
const valueTypeOf = value?.constructor?.name || typeof value;
|
|
77
|
+
errors.push(`${fnName}: Invalid argument '${schema.name}' at position ${position}, expected ${schema.type}, got ${valueTypeOf}`);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (errors.length > 0) {
|
|
82
|
+
throw new ArgTypesError(`Argument validation failed`, errors);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {Function} fn
|
|
88
|
+
* @param {Array} argSchema
|
|
89
|
+
* @param {string} fnName
|
|
90
|
+
* @returns {Function}
|
|
91
|
+
*/
|
|
92
|
+
export const withValidation = (fn, argSchema, fnName = 'Function') => {
|
|
93
|
+
if(!Validator.isArray(argSchema)) {
|
|
94
|
+
throw new NativeDocumentError('withValidation : argSchema must be an array');
|
|
95
|
+
}
|
|
96
|
+
return function(...args) {
|
|
97
|
+
validateArgs(args, argSchema, fn.name || fnName);
|
|
98
|
+
return fn.apply(this, args);
|
|
99
|
+
};
|
|
100
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Build configuration
|
|
2
|
+
const isProd = process.env.NODE_ENV === 'production';
|
|
3
|
+
|
|
4
|
+
const DebugManager = {
|
|
5
|
+
enabled: !isProd,
|
|
6
|
+
|
|
7
|
+
enable() {
|
|
8
|
+
this.enabled = true;
|
|
9
|
+
console.log('🔍 NativeDocument Debug Mode enabled');
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
disable() {
|
|
13
|
+
this.enabled = false;
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
log: isProd ? () => {} : function(category, message, data) {
|
|
17
|
+
if (!this.enabled) return;
|
|
18
|
+
console.group(`🔍 [${category}] ${message}`);
|
|
19
|
+
if (data) console.log(data);
|
|
20
|
+
console.trace();
|
|
21
|
+
console.groupEnd();
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
warn: isProd ? () => {} : function(category, message, data) {
|
|
25
|
+
if (!this.enabled) return;
|
|
26
|
+
console.warn(`⚠️ [${category}] ${message}`, data);
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
error: isProd ? () => {} : function(category, message, error) {
|
|
30
|
+
console.error(`❌ [${category}] ${message}`, error);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default DebugManager;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* @param {Function} fn
|
|
4
|
+
* @param {number} delay
|
|
5
|
+
* @param {{leading?:Boolean, trailing?:Boolean, debounce?:Boolean}}options
|
|
6
|
+
* @returns {(function(...[*]): void)|*}
|
|
7
|
+
*/
|
|
8
|
+
export const throttle = function(fn, delay, options = {}) {
|
|
9
|
+
let timer = null;
|
|
10
|
+
let lastExecTime = 0;
|
|
11
|
+
const { leading = true, trailing = true, debounce = false } = options;
|
|
12
|
+
|
|
13
|
+
return function(...args) {
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
if (debounce) {
|
|
16
|
+
// debounce mode: reset the timer for each call
|
|
17
|
+
clearTimeout(timer);
|
|
18
|
+
timer = setTimeout(() => fn.apply(this, args), delay);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (leading && now - lastExecTime >= delay) {
|
|
22
|
+
fn.apply(this, args);
|
|
23
|
+
lastExecTime = now;
|
|
24
|
+
}
|
|
25
|
+
if (trailing && !timer) {
|
|
26
|
+
timer = setTimeout(() => {
|
|
27
|
+
fn.apply(this, args);
|
|
28
|
+
lastExecTime = Date.now();
|
|
29
|
+
timer = null;
|
|
30
|
+
}, delay - (now - lastExecTime));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const trim = function(str, char) {
|
|
36
|
+
return str.replace(new RegExp(`^[${char}]+|[${char}]+$`, 'g'), '');
|
|
37
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {withValidation} from "./args-types.js";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
Function.prototype.args = function(...args) {
|
|
5
|
+
return withValidation(this, args);
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
Function.prototype.errorBoundary = function(callback) {
|
|
9
|
+
return (...args) => {
|
|
10
|
+
try {
|
|
11
|
+
return this.apply(this, args);
|
|
12
|
+
} catch(e) {
|
|
13
|
+
return callback(e);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import ObservableItem from "../data/ObservableItem";
|
|
2
|
+
import DebugManager from "./debug-manager";
|
|
3
|
+
import NativeDocumentError from "../errors/NativeDocumentError";
|
|
4
|
+
import ObservableChecker from "../data/ObservableChecker";
|
|
5
|
+
|
|
6
|
+
const Validator = {
|
|
7
|
+
isObservable(value) {
|
|
8
|
+
return value instanceof ObservableItem || value instanceof ObservableChecker;
|
|
9
|
+
},
|
|
10
|
+
isProxy(value) {
|
|
11
|
+
return value?.__isProxy__
|
|
12
|
+
},
|
|
13
|
+
isObservableChecker(value) {
|
|
14
|
+
return value instanceof ObservableChecker;
|
|
15
|
+
},
|
|
16
|
+
isArray(value) {
|
|
17
|
+
return Array.isArray(value);
|
|
18
|
+
},
|
|
19
|
+
isString(value) {
|
|
20
|
+
return typeof value === 'string';
|
|
21
|
+
},
|
|
22
|
+
isNumber(value) {
|
|
23
|
+
return typeof value === 'number';
|
|
24
|
+
},
|
|
25
|
+
isBoolean(value) {
|
|
26
|
+
return typeof value === 'boolean';
|
|
27
|
+
},
|
|
28
|
+
isFunction(value) {
|
|
29
|
+
return typeof value === 'function';
|
|
30
|
+
},
|
|
31
|
+
isObject(value) {
|
|
32
|
+
return typeof value === 'object';
|
|
33
|
+
},
|
|
34
|
+
isJson(value) {
|
|
35
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
36
|
+
},
|
|
37
|
+
isElement(value) {
|
|
38
|
+
return value instanceof HTMLElement || value instanceof DocumentFragment || value instanceof Text;
|
|
39
|
+
},
|
|
40
|
+
isFragment(value) {
|
|
41
|
+
return value instanceof DocumentFragment;
|
|
42
|
+
},
|
|
43
|
+
isStringOrObservable(value) {
|
|
44
|
+
return this.isString(value) || this.isObservable(value);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
isValidChild(child) {
|
|
48
|
+
return child === null ||
|
|
49
|
+
this.isElement(child) ||
|
|
50
|
+
this.isObservable(child) ||
|
|
51
|
+
['string', 'number', 'boolean'].includes(typeof child);
|
|
52
|
+
},
|
|
53
|
+
isValidChildren(children) {
|
|
54
|
+
if (!Array.isArray(children)) {
|
|
55
|
+
children = [children];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const invalid = children.filter(child => !this.isValidChild(child));
|
|
59
|
+
return invalid.length === 0;
|
|
60
|
+
},
|
|
61
|
+
validateChildren(children) {
|
|
62
|
+
if (!Array.isArray(children)) {
|
|
63
|
+
children = [children];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const invalid = children.filter(child => !this.isValidChild(child));
|
|
67
|
+
if (invalid.length > 0) {
|
|
68
|
+
throw new NativeDocumentError(`Invalid children detected: ${invalid.map(i => typeof i).join(', ')}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return children;
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
validateAttributes(attributes) {
|
|
75
|
+
if (!attributes || typeof attributes !== 'object') {
|
|
76
|
+
return attributes;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const reserved = [];
|
|
80
|
+
const foundReserved = Object.keys(attributes).filter(key => reserved.includes(key));
|
|
81
|
+
|
|
82
|
+
if (foundReserved.length > 0) {
|
|
83
|
+
DebugManager.warn('Validator', `Reserved attributes found: ${foundReserved.join(', ')}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return attributes;
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
validateEventCallback(callback) {
|
|
90
|
+
if (typeof callback !== 'function') {
|
|
91
|
+
throw new NativeDocumentError('Event callback must be a function');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export default Validator;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import Validator from "../utils/validator";
|
|
2
|
+
import NativeDocumentError from "../errors/NativeDocumentError";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
*
|
|
7
|
+
* @param {HTMLElement} element
|
|
8
|
+
* @param {string} className
|
|
9
|
+
* @param {string} value
|
|
10
|
+
*/
|
|
11
|
+
const toggleClassItem = function(element, className, value) {
|
|
12
|
+
if(value) {
|
|
13
|
+
element.classList.add(className);
|
|
14
|
+
} else {
|
|
15
|
+
element.classList.remove(className);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
*
|
|
21
|
+
* @param {HTMLElement} element
|
|
22
|
+
* @param {Object} data
|
|
23
|
+
*/
|
|
24
|
+
function bindClassAttribute(element, data) {
|
|
25
|
+
for(let className in data) {
|
|
26
|
+
const value = data[className];
|
|
27
|
+
if(Validator.isObservable(value)) {
|
|
28
|
+
toggleClassItem(element, className, value.val());
|
|
29
|
+
value.subscribe(newValue => toggleClassItem(element, className, newValue));
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
toggleClassItem(element, className, value);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
*
|
|
38
|
+
* @param {HTMLElement} element
|
|
39
|
+
* @param {Object} data
|
|
40
|
+
*/
|
|
41
|
+
function bindStyleAttribute(element, data) {
|
|
42
|
+
for(let styleName in data) {
|
|
43
|
+
const value = data[styleName];
|
|
44
|
+
if(Validator.isObservable(value)) {
|
|
45
|
+
element.style[styleName] = value.val();
|
|
46
|
+
value.subscribe(newValue => {
|
|
47
|
+
element.style[styleName] = newValue;
|
|
48
|
+
});
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
element.style[styleName] = value;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
*
|
|
57
|
+
* @param {HTMLElement} element
|
|
58
|
+
* @param {Object} attributes
|
|
59
|
+
*/
|
|
60
|
+
export default function AttributesWrapper(element, attributes) {
|
|
61
|
+
|
|
62
|
+
Validator.validateAttributes(attributes);
|
|
63
|
+
|
|
64
|
+
if(!Validator.isObject(attributes)) {
|
|
65
|
+
console.log(attributes);
|
|
66
|
+
throw new NativeDocumentError('Attributes must be an object');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for(let attributeName in attributes) {
|
|
70
|
+
const value = attributes[attributeName];
|
|
71
|
+
if(Validator.isObservable(value)) {
|
|
72
|
+
value.subscribe(newValue => element.setAttribute(attributeName, newValue));
|
|
73
|
+
element.setAttribute(attributeName, value.val());
|
|
74
|
+
if(attributeName === 'value') {
|
|
75
|
+
if(['checkbox', 'radio'].includes(element.type)) {
|
|
76
|
+
element.addEventListener('input', () => value.set(element.checked));
|
|
77
|
+
} else {
|
|
78
|
+
element.addEventListener('input', () => value.set(element.value));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if(attributeName === 'class' && Validator.isJson(value)) {
|
|
84
|
+
bindClassAttribute(element, value);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if(attributeName === 'style' && Validator.isJson(value)) {
|
|
88
|
+
bindStyleAttribute(element, value);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
element.setAttribute(attributeName, value);
|
|
92
|
+
}
|
|
93
|
+
return element;
|
|
94
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {throttle} from "../utils/helpers";
|
|
2
|
+
|
|
3
|
+
const DocumentObserver = {
|
|
4
|
+
elements: new Map(),
|
|
5
|
+
observer: null,
|
|
6
|
+
checkMutation: throttle(function() {
|
|
7
|
+
for(const [element, data] of DocumentObserver.elements.entries()) {
|
|
8
|
+
const isCurrentlyInDom = document.body.contains(element);
|
|
9
|
+
if(isCurrentlyInDom && !data.inDom) {
|
|
10
|
+
data.inDom = true;
|
|
11
|
+
data.mounted.forEach(callback => callback(element));
|
|
12
|
+
} else if(!isCurrentlyInDom && data.inDom) {
|
|
13
|
+
data.inDom = false;
|
|
14
|
+
data.unmounted.forEach(callback => callback(element));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}, 10, { debounce: true }),
|
|
18
|
+
/**
|
|
19
|
+
*
|
|
20
|
+
* @param {HTMLElement} element
|
|
21
|
+
* @returns {{watch: (function(): Map<any, any>), disconnect: (function(): boolean), mounted: (function(*): Set<any>), unmounted: (function(*): Set<any>)}}
|
|
22
|
+
*/
|
|
23
|
+
watch: function(element) {
|
|
24
|
+
let data = {};
|
|
25
|
+
if(DocumentObserver.elements.has(element)) {
|
|
26
|
+
data = DocumentObserver.elements.get(element);
|
|
27
|
+
} else {
|
|
28
|
+
const inDom = document.body.contains(element);
|
|
29
|
+
data = {
|
|
30
|
+
inDom,
|
|
31
|
+
mounted: new Set(),
|
|
32
|
+
unmounted: new Set(),
|
|
33
|
+
};
|
|
34
|
+
DocumentObserver.elements.set(element, data);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
watch: () => DocumentObserver.elements.set(element, data),
|
|
39
|
+
disconnect: () => DocumentObserver.elements.delete(element),
|
|
40
|
+
mounted: (callback) => data.mounted.add(callback),
|
|
41
|
+
unmounted: (callback) => data.unmounted.add(callback)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
DocumentObserver.observer = new MutationObserver(DocumentObserver.checkMutation);
|
|
47
|
+
DocumentObserver.observer.observe(document.body, {
|
|
48
|
+
childList: true,
|
|
49
|
+
subtree: true,
|
|
50
|
+
});
|
|
51
|
+
export default DocumentObserver;
|