@gogocat/data-bind 1.11.0 → 2.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/.editorconfig +14 -14
- package/.vscode/launch.json +12 -12
- package/CONFIGURATION.md +294 -0
- package/REACTIVE_MODE.md +553 -0
- package/README.md +266 -829
- package/babel.config.json +30 -0
- package/dist/js/_escape.d.ts +14 -0
- package/dist/js/_escape.d.ts.map +1 -0
- package/dist/js/applyBinding.d.ts +11 -0
- package/dist/js/applyBinding.d.ts.map +1 -0
- package/dist/js/attrBinding.d.ts +12 -0
- package/dist/js/attrBinding.d.ts.map +1 -0
- package/dist/js/binder.d.ts +67 -0
- package/dist/js/binder.d.ts.map +1 -0
- package/dist/js/changeBinding.d.ts +19 -0
- package/dist/js/changeBinding.d.ts.map +1 -0
- package/dist/js/commentWrapper.d.ts +39 -0
- package/dist/js/commentWrapper.d.ts.map +1 -0
- package/dist/js/config.d.ts +55 -0
- package/dist/js/config.d.ts.map +1 -0
- package/dist/js/createBindingOption.d.ts +32 -0
- package/dist/js/createBindingOption.d.ts.map +1 -0
- package/dist/js/createEventBinding.d.ts +10 -0
- package/dist/js/createEventBinding.d.ts.map +1 -0
- package/dist/js/cssBinding.d.ts +15 -0
- package/dist/js/cssBinding.d.ts.map +1 -0
- package/dist/js/dataBind.js +2772 -2519
- package/dist/js/dataBind.min.js +8 -1
- package/dist/js/dataBind.min.js.map +1 -1
- package/dist/js/domWalker.d.ts +9 -0
- package/dist/js/domWalker.d.ts.map +1 -0
- package/dist/js/forOfBinding.d.ts +12 -0
- package/dist/js/forOfBinding.d.ts.map +1 -0
- package/dist/js/hoverBinding.d.ts +13 -0
- package/dist/js/hoverBinding.d.ts.map +1 -0
- package/dist/js/ifBinding.d.ts +12 -0
- package/dist/js/ifBinding.d.ts.map +1 -0
- package/dist/js/index.d.ts +10 -0
- package/dist/js/index.d.ts.map +1 -0
- package/dist/js/modelBinding.d.ts +12 -0
- package/dist/js/modelBinding.d.ts.map +1 -0
- package/dist/js/postProcess.d.ts +3 -0
- package/dist/js/postProcess.d.ts.map +1 -0
- package/dist/js/pubSub.d.ts +11 -0
- package/dist/js/pubSub.d.ts.map +1 -0
- package/dist/js/reactiveProxy.d.ts +28 -0
- package/dist/js/reactiveProxy.d.ts.map +1 -0
- package/dist/js/renderForOfBinding.d.ts +8 -0
- package/dist/js/renderForOfBinding.d.ts.map +1 -0
- package/dist/js/renderIfBinding.d.ts +22 -0
- package/dist/js/renderIfBinding.d.ts.map +1 -0
- package/dist/js/renderIteration.d.ts +16 -0
- package/dist/js/renderIteration.d.ts.map +1 -0
- package/dist/js/renderTemplate.d.ts +14 -0
- package/dist/js/renderTemplate.d.ts.map +1 -0
- package/dist/js/renderTemplatesBinding.d.ts +19 -0
- package/dist/js/renderTemplatesBinding.d.ts.map +1 -0
- package/dist/js/showBinding.d.ts +13 -0
- package/dist/js/showBinding.d.ts.map +1 -0
- package/dist/js/switchBinding.d.ts +13 -0
- package/dist/js/switchBinding.d.ts.map +1 -0
- package/dist/js/textBinding.d.ts +13 -0
- package/dist/js/textBinding.d.ts.map +1 -0
- package/dist/js/types/_escape.d.ts +14 -0
- package/dist/js/types/_escape.d.ts.map +1 -0
- package/dist/js/types/applyBinding.d.ts +11 -0
- package/dist/js/types/applyBinding.d.ts.map +1 -0
- package/dist/js/types/attrBinding.d.ts +12 -0
- package/dist/js/types/attrBinding.d.ts.map +1 -0
- package/dist/js/types/binder.d.ts +67 -0
- package/dist/js/types/binder.d.ts.map +1 -0
- package/dist/js/types/changeBinding.d.ts +19 -0
- package/dist/js/types/changeBinding.d.ts.map +1 -0
- package/dist/js/types/commentWrapper.d.ts +39 -0
- package/dist/js/types/commentWrapper.d.ts.map +1 -0
- package/dist/js/types/config.d.ts +55 -0
- package/dist/js/types/config.d.ts.map +1 -0
- package/dist/js/types/createBindingOption.d.ts +32 -0
- package/dist/js/types/createBindingOption.d.ts.map +1 -0
- package/dist/js/types/createEventBinding.d.ts +10 -0
- package/dist/js/types/createEventBinding.d.ts.map +1 -0
- package/dist/js/types/cssBinding.d.ts +15 -0
- package/dist/js/types/cssBinding.d.ts.map +1 -0
- package/dist/js/types/domWalker.d.ts +9 -0
- package/dist/js/types/domWalker.d.ts.map +1 -0
- package/dist/js/types/forOfBinding.d.ts +12 -0
- package/dist/js/types/forOfBinding.d.ts.map +1 -0
- package/dist/js/types/hoverBinding.d.ts +13 -0
- package/dist/js/types/hoverBinding.d.ts.map +1 -0
- package/dist/js/types/ifBinding.d.ts +12 -0
- package/dist/js/types/ifBinding.d.ts.map +1 -0
- package/dist/js/types/index.d.ts +10 -0
- package/dist/js/types/index.d.ts.map +1 -0
- package/dist/js/types/modelBinding.d.ts +12 -0
- package/dist/js/types/modelBinding.d.ts.map +1 -0
- package/dist/js/types/postProcess.d.ts +3 -0
- package/dist/js/types/postProcess.d.ts.map +1 -0
- package/dist/js/types/pubSub.d.ts +11 -0
- package/dist/js/types/pubSub.d.ts.map +1 -0
- package/dist/js/types/reactiveProxy.d.ts +28 -0
- package/dist/js/types/reactiveProxy.d.ts.map +1 -0
- package/dist/js/types/renderForOfBinding.d.ts +8 -0
- package/dist/js/types/renderForOfBinding.d.ts.map +1 -0
- package/dist/js/types/renderIfBinding.d.ts +22 -0
- package/dist/js/types/renderIfBinding.d.ts.map +1 -0
- package/dist/js/types/renderIteration.d.ts +16 -0
- package/dist/js/types/renderIteration.d.ts.map +1 -0
- package/dist/js/types/renderTemplate.d.ts +14 -0
- package/dist/js/types/renderTemplate.d.ts.map +1 -0
- package/dist/js/types/renderTemplatesBinding.d.ts +19 -0
- package/dist/js/types/renderTemplatesBinding.d.ts.map +1 -0
- package/dist/js/types/showBinding.d.ts +13 -0
- package/dist/js/types/showBinding.d.ts.map +1 -0
- package/dist/js/types/switchBinding.d.ts +13 -0
- package/dist/js/types/switchBinding.d.ts.map +1 -0
- package/dist/js/types/textBinding.d.ts +13 -0
- package/dist/js/types/textBinding.d.ts.map +1 -0
- package/dist/js/types/types.d.ts +111 -0
- package/dist/js/types/types.d.ts.map +1 -0
- package/dist/js/types/util.d.ts +119 -0
- package/dist/js/types/util.d.ts.map +1 -0
- package/dist/js/types.d.ts +111 -0
- package/dist/js/types.d.ts.map +1 -0
- package/dist/js/util.d.ts +119 -0
- package/dist/js/util.d.ts.map +1 -0
- package/eslint.config.js +124 -0
- package/examples/DBMONSTER_COMPARISON.md +123 -0
- package/examples/afterRenderDemo.html +119 -0
- package/examples/bootstrap/css/animate.css +1579 -1579
- package/examples/bootstrap/css/bootstrap.min.css +6 -6
- package/examples/bootstrap/css/homeservices.css +378 -390
- package/examples/bootstrap/css/open-iconic.css +511 -511
- package/examples/bootstrap/fonts/open-iconic.svg +543 -543
- package/examples/bootstrap/js/compMessageDialog.js +20 -19
- package/examples/bootstrap/js/compSearchBar.js +12 -19
- package/examples/bootstrap/js/compSearchResults.js +50 -46
- package/examples/bootstrap/js/featureAdsResult.json +65 -65
- package/examples/bootstrap/js/searchResult.json +57 -57
- package/examples/bootstrap.html +343 -332
- package/examples/css/baseTodo.css +141 -141
- package/examples/css/dbMonsterStyles.css +27 -27
- package/examples/css/indexTodo.css +374 -374
- package/examples/dbmonsterForOfReactive.html +40 -0
- package/examples/dbmonsterReact.html +19 -0
- package/examples/forOfBindingSimpleDebug.html +45 -0
- package/examples/form.html +20 -4
- package/examples/globalConfig.html +131 -0
- package/examples/js/afterRenderDemo.js +190 -0
- package/examples/js/appTodo.js +46 -46
- package/examples/js/attrBindingDemo.js +2 -2
- package/examples/js/dbMonApp.js +24 -26
- package/examples/js/dbMonAppReact.jsx +79 -0
- package/examples/js/dbMonAppReactive.js +28 -0
- package/examples/js/fiberDemo.js +4 -4
- package/examples/js/filtersDemo.js +8 -8
- package/examples/js/forOfDemo.js +7 -9
- package/examples/js/forOfDemoComplex.js +44 -17
- package/examples/js/form.js +44 -12
- package/examples/js/globalConfig.js +117 -0
- package/examples/js/ifBindingDemo.js +16 -16
- package/examples/js/reactiveDemo.js +119 -0
- package/examples/js/switchBindingDemo.js +8 -8
- package/examples/react-dbmonster/dist/bundle.js +43 -0
- package/examples/react-dbmonster/package-lock.json +537 -0
- package/examples/react-dbmonster/package.json +16 -0
- package/examples/react-dbmonster/src/index.jsx +80 -0
- package/examples/reactiveDemo.html +127 -0
- package/examples/refreshRateTest.html +75 -75
- package/index.html +841 -0
- package/package.json +31 -34
- package/rollup.config.js +79 -36
- package/src/{_escape.js → _escape.ts} +19 -17
- package/src/applyBinding.ts +179 -0
- package/src/{attrBinding.js → attrBinding.ts} +14 -13
- package/src/binder.ts +289 -0
- package/src/changeBinding.ts +93 -0
- package/src/{commentWrapper.js → commentWrapper.ts} +33 -30
- package/src/config.ts +107 -0
- package/src/createBindingOption.ts +91 -0
- package/src/createEventBinding.ts +88 -0
- package/src/{cssBinding.js → cssBinding.ts} +13 -11
- package/src/{domWalker.js → domWalker.ts} +44 -30
- package/src/{forOfBinding.js → forOfBinding.ts} +4 -3
- package/src/hoverBinding.ts +84 -0
- package/src/{ifBinding.js → ifBinding.ts} +14 -12
- package/src/index.ts +53 -0
- package/src/{modelBinding.js → modelBinding.ts} +11 -9
- package/src/postProcess.ts +22 -0
- package/src/{pubSub.js → pubSub.ts} +24 -15
- package/src/reactiveProxy.ts +285 -0
- package/src/{renderForOfBinding.js → renderForOfBinding.ts} +55 -33
- package/src/{renderIfBinding.js → renderIfBinding.ts} +45 -20
- package/src/renderIteration.ts +53 -0
- package/src/renderTemplate.ts +165 -0
- package/src/renderTemplatesBinding.ts +73 -0
- package/src/{showBinding.js → showBinding.ts} +4 -3
- package/src/{switchBinding.js → switchBinding.ts} +18 -15
- package/src/{textBinding.js → textBinding.ts} +5 -4
- package/src/types.ts +124 -0
- package/src/util.ts +810 -0
- package/test/css/reporter.css +9 -9
- package/test/fixtures/dataBindBootstrap.html +2 -2
- package/test/fixtures/formBindings.html +9 -1
- package/test/globals.d.ts +19 -0
- package/test/helpers/testHelper.js +46 -11
- package/test/mocks/featureAdsResult.json +65 -65
- package/test/mocks/searchResult.json +57 -57
- package/test/specs/{attrBinding.spec.js → attrBinding.spec.ts} +103 -106
- package/test/specs/{binder.spec.js → binder.spec.ts} +29 -27
- package/test/specs/blurBinding.spec.ts +60 -0
- package/test/specs/chainableUse.spec.ts +125 -0
- package/test/specs/clickBinding.spec.ts +194 -0
- package/test/specs/{cssBinding.spec.js → cssBinding.spec.ts} +72 -79
- package/test/specs/{dataBindBootstrap.spec.js → dataBindBootstrap.spec.ts} +332 -313
- package/test/specs/{filter.spec.js → filter.spec.ts} +75 -76
- package/test/specs/{forOfBinding.spec.js → forOfBinding.spec.ts} +208 -219
- package/test/specs/formBinding.spec.ts +272 -0
- package/test/specs/ifBinding.spec.ts +165 -0
- package/test/specs/{nestedComponent.spec.js → nestedComponent.spec.ts} +88 -88
- package/test/specs/reactiveProxy.spec.ts +465 -0
- package/test/specs/{showBinding.spec.js → showBinding.spec.ts} +148 -149
- package/test/specs/{switchBinding.spec.js → switchBinding.spec.ts} +172 -173
- package/test/specs/templateBinding.spec.ts +273 -0
- package/test/specs/{textBinding.spec.js → textBinding.spec.ts} +47 -48
- package/test/tsconfig.json +31 -0
- package/test-output.txt +200 -0
- package/test-reactive.html +224 -0
- package/tsconfig.json +28 -0
- package/vendors/lodash.custom.js +4577 -4577
- package/vendors/lodash.custom.min.js +45 -45
- package/vitest.config.js +27 -0
- package/.eslintrc.js +0 -1
- package/.grunt/grunt-contrib-jasmine/boot.js +0 -161
- package/.grunt/grunt-contrib-jasmine/dist/js/dataBind.js +0 -9
- package/.grunt/grunt-contrib-jasmine/grunt-template-jasmine-istanbul/reporter.js +0 -23
- package/.grunt/grunt-contrib-jasmine/jasmine-html.js +0 -853
- package/.grunt/grunt-contrib-jasmine/jasmine.css +0 -271
- package/.grunt/grunt-contrib-jasmine/jasmine.js +0 -9761
- package/.grunt/grunt-contrib-jasmine/jasmine_favicon.png +0 -0
- package/.grunt/grunt-contrib-jasmine/json2.js +0 -489
- package/.grunt/grunt-contrib-jasmine/reporter.js +0 -107
- package/coverage/coverage.json +0 -1
- package/coverage/lcov/lcov-report/base.css +0 -213
- package/coverage/lcov/lcov-report/index.html +0 -93
- package/coverage/lcov/lcov-report/js/dataBind.js.html +0 -6596
- package/coverage/lcov/lcov-report/js/index.html +0 -93
- package/coverage/lcov/lcov-report/prettify.css +0 -1
- package/coverage/lcov/lcov-report/prettify.js +0 -1
- package/coverage/lcov/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov/lcov-report/sorter.js +0 -158
- package/coverage/lcov/lcov.info +0 -1991
- package/eslintrc.json +0 -40
- package/examples/bootstrap/js/bootstrap.min.js +0 -6
- package/examples/bootstrap/js/popper.min.js +0 -5
- package/examples/bootstrap/js/searchSuggestion.js +0 -58
- package/examples/bootstrap/js/typeahead.jquery.js +0 -1538
- package/gruntfile.js +0 -92
- package/gulpfile.js +0 -32
- package/src/binder.js +0 -422
- package/src/changeBinding.js +0 -57
- package/src/config.js +0 -65
- package/src/createBindingOption.js +0 -66
- package/src/createEventBinding.js +0 -46
- package/src/eventSystem.js +0 -46
- package/src/hoverBinding.js +0 -57
- package/src/index.js +0 -26
- package/src/renderTemplate.js +0 -128
- package/src/util.js +0 -648
- package/test/specs/blurBinding.spec.js +0 -57
- package/test/specs/formBinding.spec.js +0 -292
- package/test/specs/ifBinding.spec.js +0 -169
- package/test/specs/templateBinding.spec.js +0 -117
- package/vendors/jasmine-jquery.js +0 -841
- package/vendors/jquery-3.2.1.min.js +0 -4
package/src/util.ts
ADDED
|
@@ -0,0 +1,810 @@
|
|
|
1
|
+
import * as config from './config';
|
|
2
|
+
import type {ViewModel, BindingCache, ElementCache, DeferredObj, WrapMap, PlainObject} from './types';
|
|
3
|
+
|
|
4
|
+
const hasIsArray = Array.isArray;
|
|
5
|
+
|
|
6
|
+
export const REGEX = {
|
|
7
|
+
BAD_TAGS: /<(script|del)(?=[\s>])[\w\W]*?<\/\1\s*>/ig,
|
|
8
|
+
FOR_OF: /(.*?)\s+(?:in|of)\s+(.*)/,
|
|
9
|
+
FUNCTION_PARAM: /\((.*?)\)/,
|
|
10
|
+
HTML_TAG: /^[\s]*<([a-z][^\/\s>]+)/i,
|
|
11
|
+
OBJECT_LITERAL: /^\{.+\}$/,
|
|
12
|
+
PIPE: /\|/,
|
|
13
|
+
WHITE_SPACES: /\s+/g,
|
|
14
|
+
LINE_BREAKS_TABS: /(\r\n|\n|\r|\t)/gm,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const IS_SUPPORT_TEMPLATE = 'content' in document.createElement('template');
|
|
18
|
+
|
|
19
|
+
const WRAP_MAP: WrapMap = {
|
|
20
|
+
div: ['div', '<div>', '</div>'],
|
|
21
|
+
thead: ['table', '<table>', '</table>'],
|
|
22
|
+
col: ['colgroup', '<table><colgroup>', '</colgroup></table>'],
|
|
23
|
+
tr: ['tbody', '<table><tbody>', '</tbody></table>'],
|
|
24
|
+
td: ['tr', '<table><tr>', '</tr></table>'],
|
|
25
|
+
};
|
|
26
|
+
WRAP_MAP.caption = WRAP_MAP.colgroup = WRAP_MAP.tbody = WRAP_MAP.tfoot = WRAP_MAP.thead;
|
|
27
|
+
WRAP_MAP.th = WRAP_MAP.td;
|
|
28
|
+
|
|
29
|
+
export const isArray = (obj: unknown): obj is unknown[] => {
|
|
30
|
+
return hasIsArray ? Array.isArray(obj) : Object.prototype.toString.call(obj) === '[object Array]';
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const isJsObject = (obj: unknown): obj is object => {
|
|
34
|
+
return obj !== null && typeof obj === 'object' && Object.prototype.toString.call(obj) === '[object Object]';
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const isPlainObject = (obj: unknown): obj is PlainObject => {
|
|
38
|
+
if (!isJsObject(obj)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// If has modified constructor
|
|
43
|
+
const ctor = (obj as PlainObject).constructor;
|
|
44
|
+
if (typeof ctor !== 'function') return false;
|
|
45
|
+
|
|
46
|
+
// If has modified prototype
|
|
47
|
+
const prot = ctor.prototype;
|
|
48
|
+
if (isJsObject(prot) === false) return false;
|
|
49
|
+
|
|
50
|
+
// If constructor does not have an Object-specific method
|
|
51
|
+
if (prot.hasOwnProperty('isPrototypeOf') === false) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Most likely a plain Object
|
|
56
|
+
return true;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// test if string contains '{...}'. string must not contains tab, line breaks
|
|
60
|
+
export const isObjectLiteralString = (str: string = ''): boolean => {
|
|
61
|
+
return REGEX.OBJECT_LITERAL.test(str);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const isEmptyObject = (obj: unknown): boolean => {
|
|
65
|
+
if (isJsObject(obj)) {
|
|
66
|
+
return Object.getOwnPropertyNames(obj).length === 0;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const getFirstHtmlStringTag = (htmlString: string): string | null => {
|
|
72
|
+
const match = htmlString.match(REGEX.HTML_TAG);
|
|
73
|
+
if (match) {
|
|
74
|
+
return match[1];
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const removeBadTags = (htmlString: string = ''): string => {
|
|
80
|
+
return htmlString.replace(REGEX.BAD_TAGS, '');
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const createHtmlFragment = (htmlString: unknown): DocumentFragment | null => {
|
|
84
|
+
if (typeof htmlString !== 'string') {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
// use template element
|
|
88
|
+
if (IS_SUPPORT_TEMPLATE) {
|
|
89
|
+
const template = document.createElement('template');
|
|
90
|
+
template.innerHTML = removeBadTags(htmlString);
|
|
91
|
+
return template.content;
|
|
92
|
+
}
|
|
93
|
+
// use document fragment with wrap html tag for tr, td etc.
|
|
94
|
+
const fragment = document.createDocumentFragment();
|
|
95
|
+
const queryContainer = document.createElement('div');
|
|
96
|
+
const firstTag = getFirstHtmlStringTag(htmlString);
|
|
97
|
+
const wrap = WRAP_MAP[firstTag || 'div'];
|
|
98
|
+
|
|
99
|
+
if (wrap[0] === 'div') {
|
|
100
|
+
return document.createRange().createContextualFragment(htmlString);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
queryContainer.insertAdjacentHTML('beforeend', `${wrap[1]}${htmlString}${wrap[2]}`);
|
|
104
|
+
|
|
105
|
+
const query = queryContainer.querySelector(wrap[0]);
|
|
106
|
+
|
|
107
|
+
while (query && query.firstChild) {
|
|
108
|
+
fragment.appendChild(query.firstChild);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return fragment;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const generateElementCache = (bindingAttrs: PlainObject | unknown[]): ElementCache => {
|
|
115
|
+
const elementCache: ElementCache = {};
|
|
116
|
+
|
|
117
|
+
for (const i in bindingAttrs) {
|
|
118
|
+
if (bindingAttrs.hasOwnProperty(i)) {
|
|
119
|
+
if (isArray(bindingAttrs)) {
|
|
120
|
+
elementCache[bindingAttrs[i] as string] = [];
|
|
121
|
+
} else {
|
|
122
|
+
elementCache[i] = [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return elementCache;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* List of dangerous property names that should not be accessed
|
|
133
|
+
* to prevent prototype pollution attacks
|
|
134
|
+
*/
|
|
135
|
+
const DANGEROUS_PROPS = ['__proto__', 'constructor', 'prototype'];
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check if a property name is safe to access
|
|
139
|
+
*/
|
|
140
|
+
const isSafeProperty = (prop: string): boolean => {
|
|
141
|
+
return !DANGEROUS_PROPS.includes(prop);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// simplified version of Lodash _.get with prototype pollution protection
|
|
145
|
+
const _get = (obj: unknown, path: string, def?: unknown): unknown => {
|
|
146
|
+
const fullPath = path
|
|
147
|
+
.replace(/\[/g, '.')
|
|
148
|
+
.replace(/]/g, '')
|
|
149
|
+
.split('.')
|
|
150
|
+
.filter(Boolean);
|
|
151
|
+
|
|
152
|
+
let current: unknown = obj;
|
|
153
|
+
for (const step of fullPath) {
|
|
154
|
+
// Prevent access to dangerous properties
|
|
155
|
+
if (!step || !isSafeProperty(step)) {
|
|
156
|
+
return def;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (current == null) {
|
|
160
|
+
return def;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
current = (current as PlainObject)[step];
|
|
164
|
+
|
|
165
|
+
if (current === undefined) {
|
|
166
|
+
return def;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return current;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* getViewModelValue
|
|
175
|
+
* @description walk a object by provided string path. eg 'a.b.c'
|
|
176
|
+
* @param {object} viewModel
|
|
177
|
+
* @param {string} prop
|
|
178
|
+
* @return {object}
|
|
179
|
+
*/
|
|
180
|
+
export const getViewModelValue = (viewModel: ViewModel, prop: string): unknown => {
|
|
181
|
+
return _get(viewModel, prop);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// simplified version of Lodash _.set with prototype pollution protection
|
|
185
|
+
// https://stackoverflow.com/questions/54733539/javascript-implementation-of-lodash-set-method
|
|
186
|
+
const _set = (obj: PlainObject, path: string | string[], value: unknown): PlainObject => {
|
|
187
|
+
if (Object(obj) !== obj) return obj; // When obj is not an object
|
|
188
|
+
|
|
189
|
+
// If not yet an array, get the keys from the string-path
|
|
190
|
+
let pathArray: string[];
|
|
191
|
+
if (!Array.isArray(path)) {
|
|
192
|
+
pathArray = path.toString().match(/[^.[\]]+/g) || [];
|
|
193
|
+
} else {
|
|
194
|
+
pathArray = path;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check all keys in path for dangerous properties
|
|
198
|
+
for (const key of pathArray) {
|
|
199
|
+
if (!isSafeProperty(key)) {
|
|
200
|
+
console.warn(`Blocked attempt to set dangerous property: ${key}`);
|
|
201
|
+
return obj;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Iterate all of them except the last one
|
|
206
|
+
const lastKey = pathArray[pathArray.length - 1];
|
|
207
|
+
const target = pathArray.slice(0, -1).reduce((a: PlainObject, c: string, i: number) => {
|
|
208
|
+
// Prevent setting dangerous properties
|
|
209
|
+
if (!isSafeProperty(c)) {
|
|
210
|
+
return a;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (Object(a[c]) === a[c]) {
|
|
214
|
+
// Key exists and is an object, follow that path
|
|
215
|
+
return a[c] as PlainObject;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Create the key. Is the next key a potential array-index?
|
|
219
|
+
const nextKey = pathArray[i + 1];
|
|
220
|
+
a[c] = Math.abs(Number(nextKey)) >> 0 === +nextKey ? [] : {};
|
|
221
|
+
return a[c] as PlainObject;
|
|
222
|
+
}, obj);
|
|
223
|
+
|
|
224
|
+
// Set the final value only if the key is safe
|
|
225
|
+
if (isSafeProperty(lastKey)) {
|
|
226
|
+
target[lastKey] = value;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Return the top-level object to allow chaining
|
|
230
|
+
return obj;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* setViewModelValue
|
|
235
|
+
* @description populate viewModel object by path string
|
|
236
|
+
* @param {object} obj
|
|
237
|
+
* @param {string} prop
|
|
238
|
+
* @param {string} value
|
|
239
|
+
* @return {call} underscore set
|
|
240
|
+
*/
|
|
241
|
+
export const setViewModelValue = (obj: PlainObject, prop: string, value: unknown): PlainObject => {
|
|
242
|
+
return _set(obj, prop, value);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
export const getViewModelPropValue = (viewModel: ViewModel, bindingCache: BindingCache): unknown => {
|
|
246
|
+
let dataKey = bindingCache.dataKey;
|
|
247
|
+
let paramList = bindingCache.parameters;
|
|
248
|
+
const isInvertBoolean = dataKey && dataKey.charAt(0) === '!';
|
|
249
|
+
|
|
250
|
+
if (isInvertBoolean && dataKey) {
|
|
251
|
+
dataKey = isInvertBoolean ? dataKey.substring(1) : dataKey;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let ret = dataKey ? getViewModelValue(viewModel, dataKey) : undefined;
|
|
255
|
+
|
|
256
|
+
if (typeof ret === 'function') {
|
|
257
|
+
const viewModelContext = resolveViewModelContext(viewModel, dataKey || '');
|
|
258
|
+
const oldViewModelProValue = bindingCache.elementData ? bindingCache.elementData.viewModelPropValue : null;
|
|
259
|
+
paramList = paramList ? resolveParamList(viewModel, paramList) : [];
|
|
260
|
+
// let args = [oldViewModelProValue, bindingCache.el].concat(paramList);
|
|
261
|
+
const args = paramList.concat([oldViewModelProValue, bindingCache.el]);
|
|
262
|
+
ret = (ret as Function).apply(viewModelContext, args);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
ret = isInvertBoolean ? !ret : ret;
|
|
266
|
+
|
|
267
|
+
// call through fitlers to get final value
|
|
268
|
+
ret = filtersViewModelPropValue({
|
|
269
|
+
value: ret,
|
|
270
|
+
viewModel,
|
|
271
|
+
bindingCache,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
return ret;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const filtersViewModelPropValue = ({value, viewModel, bindingCache}: {value: unknown, viewModel: ViewModel, bindingCache: BindingCache}): unknown => {
|
|
278
|
+
let ret = value;
|
|
279
|
+
if (bindingCache.filters) {
|
|
280
|
+
each(bindingCache.filters, (index: string | number, filter: string) => {
|
|
281
|
+
const viewModelContext = resolveViewModelContext(viewModel, filter);
|
|
282
|
+
const filterFn = getViewModelValue.call(viewModelContext, viewModelContext, filter);
|
|
283
|
+
try {
|
|
284
|
+
ret = (filterFn as Function).call(viewModelContext, ret);
|
|
285
|
+
} catch (err) {
|
|
286
|
+
throwErrorMessage(err, `Invalid filter: ${filter}`);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
return ret;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
export const parseStringToJson = (str: string): PlainObject => {
|
|
294
|
+
// fix unquote or single quote keys and replace single quote to double quote
|
|
295
|
+
const ret = str.replace(/(\s*?{\s*?|\s*?,\s*?)(['"])?([a-zA-Z0-9]+)(['"])?:/g, '$1"$3":').replace(/'/g, '"');
|
|
296
|
+
return JSON.parse(ret) as PlainObject;
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* arrayRemoveMatch
|
|
301
|
+
* @description remove match items in fromArray out of toArray
|
|
302
|
+
* @param {array} toArray
|
|
303
|
+
* @param {array} frommArray
|
|
304
|
+
* @return {boolean}
|
|
305
|
+
*/
|
|
306
|
+
export const arrayRemoveMatch = (toArray: unknown[], frommArray: unknown[]): unknown[] => {
|
|
307
|
+
return toArray.filter((value, _index) => {
|
|
308
|
+
return frommArray.indexOf(value) < 0;
|
|
309
|
+
});
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
export const getFormData = ($form: HTMLFormElement): PlainObject => {
|
|
313
|
+
const data: PlainObject = {};
|
|
314
|
+
|
|
315
|
+
if (!($form instanceof HTMLFormElement)) {
|
|
316
|
+
return data;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const formData = new FormData($form);
|
|
320
|
+
|
|
321
|
+
formData.forEach((value, key) => {
|
|
322
|
+
if (!Object.prototype.hasOwnProperty.call(Object, key)) {
|
|
323
|
+
data[key] = value;
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (!Array.isArray(data[key])) {
|
|
327
|
+
data[key] = [data[key]];
|
|
328
|
+
}
|
|
329
|
+
(data[key] as unknown[]).push(value);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
return data;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* getFunctionParameterList
|
|
337
|
+
* @description convert parameter string to arrary
|
|
338
|
+
* eg. '("a","b","c")' > ["a","b","c"]
|
|
339
|
+
* @param {string} str
|
|
340
|
+
* @return {array} paramlist
|
|
341
|
+
*/
|
|
342
|
+
export const getFunctionParameterList = (str: string): string[] | undefined => {
|
|
343
|
+
if (!str || str.length > config.maxDatakeyLength) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const paramlist = str.match(REGEX.FUNCTION_PARAM);
|
|
347
|
+
|
|
348
|
+
if (paramlist && paramlist[1]) {
|
|
349
|
+
const params = paramlist[1].split(',');
|
|
350
|
+
params.forEach((v, i) => {
|
|
351
|
+
params[i] = v.trim();
|
|
352
|
+
});
|
|
353
|
+
return params;
|
|
354
|
+
}
|
|
355
|
+
return undefined;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
export const extractFilterList = (cacheData: Partial<BindingCache>): Partial<BindingCache> => {
|
|
359
|
+
if (!cacheData || !cacheData.dataKey || cacheData.dataKey.length > config.maxDatakeyLength) {
|
|
360
|
+
return cacheData;
|
|
361
|
+
}
|
|
362
|
+
const filterList = cacheData.dataKey.split(REGEX.PIPE);
|
|
363
|
+
let isOnceIndex: number | undefined;
|
|
364
|
+
cacheData.dataKey = filterList[0].trim();
|
|
365
|
+
if (filterList.length > 1) {
|
|
366
|
+
filterList.shift();
|
|
367
|
+
filterList.forEach((v, i) => {
|
|
368
|
+
filterList[i] = v.trim();
|
|
369
|
+
if (filterList[i] === config.constants.filters.ONCE) {
|
|
370
|
+
cacheData.isOnce = true;
|
|
371
|
+
isOnceIndex = i;
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
// don't store filter 'once' - because it is internal logic not a property from viewModel
|
|
375
|
+
if (isOnceIndex !== undefined && isOnceIndex >= 0) {
|
|
376
|
+
filterList.splice(isOnceIndex, 1);
|
|
377
|
+
}
|
|
378
|
+
cacheData.filters = filterList;
|
|
379
|
+
}
|
|
380
|
+
return cacheData;
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
export const invertObj = (sourceObj: PlainObject): PlainObject => {
|
|
384
|
+
return Object.keys(sourceObj).reduce((obj: PlainObject, key: string) => {
|
|
385
|
+
const invertedKey = sourceObj[key];
|
|
386
|
+
// Prevent prototype pollution by checking if the inverted key is safe
|
|
387
|
+
if (typeof invertedKey === 'string' && isSafeProperty(invertedKey)) {
|
|
388
|
+
obj[invertedKey] = key;
|
|
389
|
+
}
|
|
390
|
+
return obj;
|
|
391
|
+
}, {});
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
export const createDeferredObj = (): DeferredObj => {
|
|
395
|
+
const dfObj = {} as DeferredObj;
|
|
396
|
+
|
|
397
|
+
dfObj.promise = new Promise((resolve, reject) => {
|
|
398
|
+
dfObj.resolve = resolve;
|
|
399
|
+
dfObj.reject = reject;
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
return dfObj;
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* debounce
|
|
407
|
+
* @description decorate a function to be debounce using requestAnimationFrame
|
|
408
|
+
* @param {function} fn
|
|
409
|
+
* @param {context} ctx
|
|
410
|
+
* @return {function}
|
|
411
|
+
*/
|
|
412
|
+
export const debounceRaf = (fn: Function, ctx: unknown = null): Function => {
|
|
413
|
+
return (function (fn: Function, ctx: unknown) {
|
|
414
|
+
let dfObj = createDeferredObj();
|
|
415
|
+
let rafId = 0;
|
|
416
|
+
|
|
417
|
+
// return decorated fn
|
|
418
|
+
return function () {
|
|
419
|
+
|
|
420
|
+
const args = Array.from ? Array.from(arguments) : Array.prototype.slice.call(arguments);
|
|
421
|
+
|
|
422
|
+
window.cancelAnimationFrame(rafId);
|
|
423
|
+
rafId = window.requestAnimationFrame(() => {
|
|
424
|
+
try {
|
|
425
|
+
// fn is Binder.render function
|
|
426
|
+
fn.apply(ctx, args);
|
|
427
|
+
// dfObj.resolve is function provided in .then promise chain
|
|
428
|
+
// ctx is the current component
|
|
429
|
+
dfObj.resolve(ctx);
|
|
430
|
+
} catch (err) {
|
|
431
|
+
console.error('error in rendering: ', err);
|
|
432
|
+
dfObj.reject(err);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// reset dfObj - otherwise then callbacks will not be in execution order
|
|
436
|
+
// example:
|
|
437
|
+
// myApp.render().then(function(){console.log('ok1')});
|
|
438
|
+
// myApp.render().then(function(){console.log('ok2')});
|
|
439
|
+
// myApp.render().then(function(){console.log('ok3')});
|
|
440
|
+
// >> ok1, ok2, ok3
|
|
441
|
+
dfObj = createDeferredObj();
|
|
442
|
+
|
|
443
|
+
window.cancelAnimationFrame(rafId);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
return dfObj.promise;
|
|
447
|
+
};
|
|
448
|
+
})(fn, ctx);
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* getNodeAttrObj
|
|
453
|
+
* @description convert Node attributes object to a json object
|
|
454
|
+
* @param {object} node
|
|
455
|
+
* @param {array} skipList
|
|
456
|
+
* @return {object}
|
|
457
|
+
*/
|
|
458
|
+
export const getNodeAttrObj = (node: HTMLElement, skipList?: string | string[]): Record<string, string> | undefined => {
|
|
459
|
+
let attributesLength = 0;
|
|
460
|
+
let skipArray: string[] | undefined;
|
|
461
|
+
|
|
462
|
+
if (!node || node.nodeType !== 1 || !node.hasAttributes()) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (skipList) {
|
|
466
|
+
skipArray = [];
|
|
467
|
+
skipArray = typeof skipList === 'string' ? [skipList] : skipList;
|
|
468
|
+
}
|
|
469
|
+
const attrObj: Record<string, string> = {};
|
|
470
|
+
attributesLength = node.attributes.length;
|
|
471
|
+
|
|
472
|
+
if (attributesLength) {
|
|
473
|
+
for (let i = 0; i < attributesLength; i += 1) {
|
|
474
|
+
const attribute = node.attributes.item(i);
|
|
475
|
+
if (attribute) {
|
|
476
|
+
attrObj[attribute.nodeName] = attribute.nodeValue || '';
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (isArray(skipArray)) {
|
|
482
|
+
skipArray.forEach((item) => {
|
|
483
|
+
if (attrObj[item]) {
|
|
484
|
+
delete attrObj[item];
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
return attrObj;
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* extend
|
|
493
|
+
* @param {boolean} isDeepMerge
|
|
494
|
+
* @param {object} target
|
|
495
|
+
* @param {object} sources
|
|
496
|
+
* @return {object} merged object
|
|
497
|
+
*/
|
|
498
|
+
export const extend = (isDeepMerge: boolean = false, target?: PlainObject, ...sources: PlainObject[]): PlainObject => {
|
|
499
|
+
if (!sources.length) {
|
|
500
|
+
return target || {};
|
|
501
|
+
}
|
|
502
|
+
const source = sources.shift();
|
|
503
|
+
if (source === undefined) {
|
|
504
|
+
return target || {};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (!isDeepMerge) {
|
|
508
|
+
return Object.assign(target || {}, source, ...sources);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (isMergebleObject(target) && isMergebleObject(source)) {
|
|
512
|
+
Object.keys(source).forEach((key) => {
|
|
513
|
+
if (isMergebleObject(source[key])) {
|
|
514
|
+
if (!target[key]) {
|
|
515
|
+
target[key] = {};
|
|
516
|
+
}
|
|
517
|
+
extend(true, target[key] as PlainObject, source[key] as PlainObject);
|
|
518
|
+
} else {
|
|
519
|
+
target[key] = source[key];
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return extend(true, target, ...sources);
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
export const each = (obj: unknown[] | PlainObject, fn: Function): void => {
|
|
528
|
+
if (typeof obj !== 'object' || typeof fn !== 'function') {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
let keys: string[] = [];
|
|
532
|
+
let keysLength = 0;
|
|
533
|
+
const isArrayObj = isArray(obj);
|
|
534
|
+
let key: string | number;
|
|
535
|
+
let value: unknown;
|
|
536
|
+
let i = 0;
|
|
537
|
+
|
|
538
|
+
if (isArrayObj) {
|
|
539
|
+
keysLength = obj.length;
|
|
540
|
+
} else if (isJsObject(obj)) {
|
|
541
|
+
keys = Object.keys(obj);
|
|
542
|
+
keysLength = keys.length;
|
|
543
|
+
} else {
|
|
544
|
+
throw new TypeError('Object is not an array or object');
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
for (i = 0; i < keysLength; i += 1) {
|
|
548
|
+
if (isArrayObj) {
|
|
549
|
+
key = i;
|
|
550
|
+
value = (obj as unknown[])[i];
|
|
551
|
+
} else {
|
|
552
|
+
key = keys[i];
|
|
553
|
+
value = (obj as PlainObject)[key];
|
|
554
|
+
}
|
|
555
|
+
fn(key, value);
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
const isMergebleObject = (item: unknown): item is PlainObject => {
|
|
560
|
+
return isJsObject(item) && !isArray(item);
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* cloneDomNode
|
|
565
|
+
* @param {object} element
|
|
566
|
+
* @return {object} cloned element
|
|
567
|
+
* @description helper function to clone node
|
|
568
|
+
*/
|
|
569
|
+
export const cloneDomNode = (element: HTMLElement): HTMLElement => {
|
|
570
|
+
return element.cloneNode(true) as HTMLElement;
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* insertAfter
|
|
575
|
+
* @param {object} parentNode
|
|
576
|
+
* @param {object} newNode
|
|
577
|
+
* @param {object} referenceNode
|
|
578
|
+
* @return {object} node
|
|
579
|
+
* @description helper function to insert new node before the reference node
|
|
580
|
+
*/
|
|
581
|
+
export const insertAfter = (parentNode: Node, newNode: Node, referenceNode: Node | null): Node => {
|
|
582
|
+
const refNextElement = referenceNode && referenceNode.nextSibling ? referenceNode.nextSibling : null;
|
|
583
|
+
return parentNode.insertBefore(newNode, refNextElement);
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
export const resolveViewModelContext = (viewModel: ViewModel, datakey: string): ViewModel => {
|
|
587
|
+
let ret = viewModel;
|
|
588
|
+
if (typeof datakey !== 'string') {
|
|
589
|
+
return ret;
|
|
590
|
+
}
|
|
591
|
+
const bindingDataContext = datakey.split('.');
|
|
592
|
+
if (bindingDataContext.length > 1) {
|
|
593
|
+
if (bindingDataContext[0] === config.bindingDataReference.rootDataKey) {
|
|
594
|
+
ret = (viewModel[config.bindingDataReference.rootDataKey] as ViewModel) || viewModel;
|
|
595
|
+
} else if (bindingDataContext[0] === config.bindingDataReference.currentData) {
|
|
596
|
+
ret = (viewModel[config.bindingDataReference.currentData] as ViewModel) || viewModel;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return ret;
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
export const resolveParamList = (viewModel: ViewModel, paramList: unknown[]): unknown[] | undefined => {
|
|
603
|
+
if (!viewModel || !isArray(paramList)) {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
return paramList.map((param) => {
|
|
607
|
+
let resolvedParam: unknown = param;
|
|
608
|
+
if (typeof param === 'string') {
|
|
609
|
+
resolvedParam = param.trim();
|
|
610
|
+
|
|
611
|
+
if (resolvedParam === config.bindingDataReference.currentIndex) {
|
|
612
|
+
// convert '$index' to value
|
|
613
|
+
resolvedParam = viewModel[config.bindingDataReference.currentIndex];
|
|
614
|
+
} else if (resolvedParam === config.bindingDataReference.currentData) {
|
|
615
|
+
// convert '$data' to value or current viewModel
|
|
616
|
+
resolvedParam = viewModel[config.bindingDataReference.currentData] || viewModel;
|
|
617
|
+
} else if (resolvedParam === config.bindingDataReference.rootDataKey) {
|
|
618
|
+
// convert '$root' to root viewModel
|
|
619
|
+
resolvedParam = viewModel[config.bindingDataReference.rootDataKey] || viewModel;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return resolvedParam;
|
|
623
|
+
});
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
export const removeElement = (el: HTMLElement): void => {
|
|
627
|
+
if (el && el.parentNode) {
|
|
628
|
+
el.parentNode.removeChild(el);
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
export const emptyElement = (node: HTMLElement): HTMLElement => {
|
|
633
|
+
if (node && node.firstChild) {
|
|
634
|
+
while (node.firstChild) {
|
|
635
|
+
node.removeChild(node.firstChild);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return node;
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* areNodesEqual
|
|
643
|
+
* @description Compare two nodes to determine if they are structurally equal
|
|
644
|
+
* @param {Node} node1
|
|
645
|
+
* @param {Node} node2
|
|
646
|
+
* @return {boolean}
|
|
647
|
+
*/
|
|
648
|
+
const areNodesEqual = (node1: Node, node2: Node): boolean => {
|
|
649
|
+
// Different node types
|
|
650
|
+
if (node1.nodeType !== node2.nodeType) {
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Text nodes - compare content
|
|
655
|
+
if (node1.nodeType === 3) {
|
|
656
|
+
return node1.nodeValue === node2.nodeValue;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Element nodes - compare tag names
|
|
660
|
+
if (node1.nodeType === 1) {
|
|
661
|
+
const el1 = node1 as HTMLElement;
|
|
662
|
+
const el2 = node2 as HTMLElement;
|
|
663
|
+
return el1.tagName === el2.tagName;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Other node types (comments, etc.)
|
|
667
|
+
return node1.nodeValue === node2.nodeValue;
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* updateElementAttributes
|
|
672
|
+
* @description Update element attributes to match new element
|
|
673
|
+
* Only updates attributes that are in the new element.
|
|
674
|
+
* Does NOT remove attributes that exist only in the existing element,
|
|
675
|
+
* as these might be runtime-added by the binding system.
|
|
676
|
+
* @param {HTMLElement} existingElement
|
|
677
|
+
* @param {HTMLElement} newElement
|
|
678
|
+
*/
|
|
679
|
+
const updateElementAttributes = (existingElement: HTMLElement, newElement: HTMLElement): void => {
|
|
680
|
+
// Get all attributes from new element
|
|
681
|
+
const newAttrs = newElement.attributes;
|
|
682
|
+
const attrsLength = newAttrs.length;
|
|
683
|
+
|
|
684
|
+
// Update or add attributes from new element
|
|
685
|
+
for (let i = 0; i < attrsLength; i += 1) {
|
|
686
|
+
const attr = newAttrs[i];
|
|
687
|
+
if (attr && attr.name) {
|
|
688
|
+
const existingValue = existingElement.getAttribute(attr.name);
|
|
689
|
+
if (existingValue !== attr.value) {
|
|
690
|
+
existingElement.setAttribute(attr.name, attr.value || '');
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// NOTE: We deliberately do NOT remove attributes that exist in the existing element
|
|
696
|
+
// but not in the new element. This preserves runtime-added attributes from the binding
|
|
697
|
+
// system (like data-bind-*, data-index, event handlers, etc.)
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* createFragmentFromChildren
|
|
702
|
+
* @description Create a DocumentFragment from a node's children
|
|
703
|
+
* @param {Node} node
|
|
704
|
+
* @return {DocumentFragment}
|
|
705
|
+
*/
|
|
706
|
+
const createFragmentFromChildren = (node: Node): DocumentFragment => {
|
|
707
|
+
const fragment = document.createDocumentFragment();
|
|
708
|
+
const children = Array.from(node.childNodes);
|
|
709
|
+
children.forEach(child => {
|
|
710
|
+
fragment.appendChild(child.cloneNode(true));
|
|
711
|
+
});
|
|
712
|
+
return fragment;
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* updateDomWithMinimalChanges
|
|
717
|
+
* @description Updates DOM by comparing existing nodes with new fragment
|
|
718
|
+
* Only modifies what changed - performs minimal DOM manipulation
|
|
719
|
+
* @param {HTMLElement} targetElement - The existing DOM element to update
|
|
720
|
+
* @param {DocumentFragment} newFragment - The new content to apply
|
|
721
|
+
*/
|
|
722
|
+
export const updateDomWithMinimalChanges = (
|
|
723
|
+
targetElement: HTMLElement,
|
|
724
|
+
newFragment: DocumentFragment,
|
|
725
|
+
): void => {
|
|
726
|
+
const newNodes = Array.from(newFragment.childNodes);
|
|
727
|
+
const existingNodes = Array.from(targetElement.childNodes);
|
|
728
|
+
const newNodesLength = newNodes.length;
|
|
729
|
+
const existingNodesLength = existingNodes.length;
|
|
730
|
+
|
|
731
|
+
// Loop through new nodes and compare with existing
|
|
732
|
+
for (let i = 0; i < newNodesLength; i += 1) {
|
|
733
|
+
const newNode = newNodes[i];
|
|
734
|
+
const existingNode = existingNodes[i];
|
|
735
|
+
|
|
736
|
+
if (!existingNode) {
|
|
737
|
+
// New node doesn't have a corresponding existing node - append it
|
|
738
|
+
targetElement.appendChild(newNode);
|
|
739
|
+
} else if (!areNodesEqual(existingNode, newNode)) {
|
|
740
|
+
// Nodes are different types or tags - replace entire node
|
|
741
|
+
targetElement.replaceChild(newNode, existingNode);
|
|
742
|
+
} else {
|
|
743
|
+
// Nodes are structurally equal - update content/attributes
|
|
744
|
+
if (newNode.nodeType === 1 && existingNode.nodeType === 1) {
|
|
745
|
+
// Element nodes - update attributes and recurse into children
|
|
746
|
+
updateElementAttributes(existingNode as HTMLElement, newNode as HTMLElement);
|
|
747
|
+
updateDomWithMinimalChanges(
|
|
748
|
+
existingNode as HTMLElement,
|
|
749
|
+
createFragmentFromChildren(newNode),
|
|
750
|
+
);
|
|
751
|
+
} else if (newNode.nodeType === 3) {
|
|
752
|
+
// Text nodes - update text content if different
|
|
753
|
+
if (existingNode.nodeValue !== newNode.nodeValue) {
|
|
754
|
+
existingNode.nodeValue = newNode.nodeValue;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Remove extra existing nodes that don't have corresponding new nodes
|
|
761
|
+
for (let i = existingNodesLength - 1; i >= newNodesLength; i -= 1) {
|
|
762
|
+
if (existingNodes[i] && existingNodes[i].parentNode) {
|
|
763
|
+
targetElement.removeChild(existingNodes[i]);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
export const throwErrorMessage = (err: unknown = null, errorMessage: string = ''): void => {
|
|
769
|
+
const message = err && typeof err === 'object' && 'message' in err ? (err as Error).message : errorMessage;
|
|
770
|
+
if (typeof console.error === 'function') {
|
|
771
|
+
console.error(message);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
console.log(message);
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* parseBindingObjectString
|
|
779
|
+
* @description parse bining object string to object with value always stringify
|
|
780
|
+
* @param {string} str - eg '{ id: $data.id, name: $data.name }'
|
|
781
|
+
* @return {object} - eg { id: '$data.id', name: '$data.name'}
|
|
782
|
+
*/
|
|
783
|
+
export const parseBindingObjectString = (str: string = ''): Record<string, string> | null => {
|
|
784
|
+
let objectLiteralString = str.trim();
|
|
785
|
+
const ret: Record<string, string> = {};
|
|
786
|
+
|
|
787
|
+
if (!REGEX.OBJECT_LITERAL.test(str)) {
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// clearn up line breaks and remove first { character
|
|
792
|
+
objectLiteralString = objectLiteralString
|
|
793
|
+
.replace(REGEX.LINE_BREAKS_TABS, '')
|
|
794
|
+
.substring(1);
|
|
795
|
+
|
|
796
|
+
// remove last } character
|
|
797
|
+
objectLiteralString = objectLiteralString.substring(0, objectLiteralString.length - 1);
|
|
798
|
+
|
|
799
|
+
objectLiteralString.split(',').forEach((item) => {
|
|
800
|
+
const keyVal = item.trim();
|
|
801
|
+
// ignore if last empty item - eg split last comma in object literal
|
|
802
|
+
if (keyVal) {
|
|
803
|
+
const prop = keyVal.split(':');
|
|
804
|
+
const key = prop[0].trim();
|
|
805
|
+
ret[key] = `${prop[1]}`.trim();
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
return ret;
|
|
810
|
+
};
|