@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.
Files changed (274) hide show
  1. package/.editorconfig +14 -14
  2. package/.vscode/launch.json +12 -12
  3. package/CONFIGURATION.md +294 -0
  4. package/REACTIVE_MODE.md +553 -0
  5. package/README.md +266 -829
  6. package/babel.config.json +30 -0
  7. package/dist/js/_escape.d.ts +14 -0
  8. package/dist/js/_escape.d.ts.map +1 -0
  9. package/dist/js/applyBinding.d.ts +11 -0
  10. package/dist/js/applyBinding.d.ts.map +1 -0
  11. package/dist/js/attrBinding.d.ts +12 -0
  12. package/dist/js/attrBinding.d.ts.map +1 -0
  13. package/dist/js/binder.d.ts +67 -0
  14. package/dist/js/binder.d.ts.map +1 -0
  15. package/dist/js/changeBinding.d.ts +19 -0
  16. package/dist/js/changeBinding.d.ts.map +1 -0
  17. package/dist/js/commentWrapper.d.ts +39 -0
  18. package/dist/js/commentWrapper.d.ts.map +1 -0
  19. package/dist/js/config.d.ts +55 -0
  20. package/dist/js/config.d.ts.map +1 -0
  21. package/dist/js/createBindingOption.d.ts +32 -0
  22. package/dist/js/createBindingOption.d.ts.map +1 -0
  23. package/dist/js/createEventBinding.d.ts +10 -0
  24. package/dist/js/createEventBinding.d.ts.map +1 -0
  25. package/dist/js/cssBinding.d.ts +15 -0
  26. package/dist/js/cssBinding.d.ts.map +1 -0
  27. package/dist/js/dataBind.js +2772 -2519
  28. package/dist/js/dataBind.min.js +8 -1
  29. package/dist/js/dataBind.min.js.map +1 -1
  30. package/dist/js/domWalker.d.ts +9 -0
  31. package/dist/js/domWalker.d.ts.map +1 -0
  32. package/dist/js/forOfBinding.d.ts +12 -0
  33. package/dist/js/forOfBinding.d.ts.map +1 -0
  34. package/dist/js/hoverBinding.d.ts +13 -0
  35. package/dist/js/hoverBinding.d.ts.map +1 -0
  36. package/dist/js/ifBinding.d.ts +12 -0
  37. package/dist/js/ifBinding.d.ts.map +1 -0
  38. package/dist/js/index.d.ts +10 -0
  39. package/dist/js/index.d.ts.map +1 -0
  40. package/dist/js/modelBinding.d.ts +12 -0
  41. package/dist/js/modelBinding.d.ts.map +1 -0
  42. package/dist/js/postProcess.d.ts +3 -0
  43. package/dist/js/postProcess.d.ts.map +1 -0
  44. package/dist/js/pubSub.d.ts +11 -0
  45. package/dist/js/pubSub.d.ts.map +1 -0
  46. package/dist/js/reactiveProxy.d.ts +28 -0
  47. package/dist/js/reactiveProxy.d.ts.map +1 -0
  48. package/dist/js/renderForOfBinding.d.ts +8 -0
  49. package/dist/js/renderForOfBinding.d.ts.map +1 -0
  50. package/dist/js/renderIfBinding.d.ts +22 -0
  51. package/dist/js/renderIfBinding.d.ts.map +1 -0
  52. package/dist/js/renderIteration.d.ts +16 -0
  53. package/dist/js/renderIteration.d.ts.map +1 -0
  54. package/dist/js/renderTemplate.d.ts +14 -0
  55. package/dist/js/renderTemplate.d.ts.map +1 -0
  56. package/dist/js/renderTemplatesBinding.d.ts +19 -0
  57. package/dist/js/renderTemplatesBinding.d.ts.map +1 -0
  58. package/dist/js/showBinding.d.ts +13 -0
  59. package/dist/js/showBinding.d.ts.map +1 -0
  60. package/dist/js/switchBinding.d.ts +13 -0
  61. package/dist/js/switchBinding.d.ts.map +1 -0
  62. package/dist/js/textBinding.d.ts +13 -0
  63. package/dist/js/textBinding.d.ts.map +1 -0
  64. package/dist/js/types/_escape.d.ts +14 -0
  65. package/dist/js/types/_escape.d.ts.map +1 -0
  66. package/dist/js/types/applyBinding.d.ts +11 -0
  67. package/dist/js/types/applyBinding.d.ts.map +1 -0
  68. package/dist/js/types/attrBinding.d.ts +12 -0
  69. package/dist/js/types/attrBinding.d.ts.map +1 -0
  70. package/dist/js/types/binder.d.ts +67 -0
  71. package/dist/js/types/binder.d.ts.map +1 -0
  72. package/dist/js/types/changeBinding.d.ts +19 -0
  73. package/dist/js/types/changeBinding.d.ts.map +1 -0
  74. package/dist/js/types/commentWrapper.d.ts +39 -0
  75. package/dist/js/types/commentWrapper.d.ts.map +1 -0
  76. package/dist/js/types/config.d.ts +55 -0
  77. package/dist/js/types/config.d.ts.map +1 -0
  78. package/dist/js/types/createBindingOption.d.ts +32 -0
  79. package/dist/js/types/createBindingOption.d.ts.map +1 -0
  80. package/dist/js/types/createEventBinding.d.ts +10 -0
  81. package/dist/js/types/createEventBinding.d.ts.map +1 -0
  82. package/dist/js/types/cssBinding.d.ts +15 -0
  83. package/dist/js/types/cssBinding.d.ts.map +1 -0
  84. package/dist/js/types/domWalker.d.ts +9 -0
  85. package/dist/js/types/domWalker.d.ts.map +1 -0
  86. package/dist/js/types/forOfBinding.d.ts +12 -0
  87. package/dist/js/types/forOfBinding.d.ts.map +1 -0
  88. package/dist/js/types/hoverBinding.d.ts +13 -0
  89. package/dist/js/types/hoverBinding.d.ts.map +1 -0
  90. package/dist/js/types/ifBinding.d.ts +12 -0
  91. package/dist/js/types/ifBinding.d.ts.map +1 -0
  92. package/dist/js/types/index.d.ts +10 -0
  93. package/dist/js/types/index.d.ts.map +1 -0
  94. package/dist/js/types/modelBinding.d.ts +12 -0
  95. package/dist/js/types/modelBinding.d.ts.map +1 -0
  96. package/dist/js/types/postProcess.d.ts +3 -0
  97. package/dist/js/types/postProcess.d.ts.map +1 -0
  98. package/dist/js/types/pubSub.d.ts +11 -0
  99. package/dist/js/types/pubSub.d.ts.map +1 -0
  100. package/dist/js/types/reactiveProxy.d.ts +28 -0
  101. package/dist/js/types/reactiveProxy.d.ts.map +1 -0
  102. package/dist/js/types/renderForOfBinding.d.ts +8 -0
  103. package/dist/js/types/renderForOfBinding.d.ts.map +1 -0
  104. package/dist/js/types/renderIfBinding.d.ts +22 -0
  105. package/dist/js/types/renderIfBinding.d.ts.map +1 -0
  106. package/dist/js/types/renderIteration.d.ts +16 -0
  107. package/dist/js/types/renderIteration.d.ts.map +1 -0
  108. package/dist/js/types/renderTemplate.d.ts +14 -0
  109. package/dist/js/types/renderTemplate.d.ts.map +1 -0
  110. package/dist/js/types/renderTemplatesBinding.d.ts +19 -0
  111. package/dist/js/types/renderTemplatesBinding.d.ts.map +1 -0
  112. package/dist/js/types/showBinding.d.ts +13 -0
  113. package/dist/js/types/showBinding.d.ts.map +1 -0
  114. package/dist/js/types/switchBinding.d.ts +13 -0
  115. package/dist/js/types/switchBinding.d.ts.map +1 -0
  116. package/dist/js/types/textBinding.d.ts +13 -0
  117. package/dist/js/types/textBinding.d.ts.map +1 -0
  118. package/dist/js/types/types.d.ts +111 -0
  119. package/dist/js/types/types.d.ts.map +1 -0
  120. package/dist/js/types/util.d.ts +119 -0
  121. package/dist/js/types/util.d.ts.map +1 -0
  122. package/dist/js/types.d.ts +111 -0
  123. package/dist/js/types.d.ts.map +1 -0
  124. package/dist/js/util.d.ts +119 -0
  125. package/dist/js/util.d.ts.map +1 -0
  126. package/eslint.config.js +124 -0
  127. package/examples/DBMONSTER_COMPARISON.md +123 -0
  128. package/examples/afterRenderDemo.html +119 -0
  129. package/examples/bootstrap/css/animate.css +1579 -1579
  130. package/examples/bootstrap/css/bootstrap.min.css +6 -6
  131. package/examples/bootstrap/css/homeservices.css +378 -390
  132. package/examples/bootstrap/css/open-iconic.css +511 -511
  133. package/examples/bootstrap/fonts/open-iconic.svg +543 -543
  134. package/examples/bootstrap/js/compMessageDialog.js +20 -19
  135. package/examples/bootstrap/js/compSearchBar.js +12 -19
  136. package/examples/bootstrap/js/compSearchResults.js +50 -46
  137. package/examples/bootstrap/js/featureAdsResult.json +65 -65
  138. package/examples/bootstrap/js/searchResult.json +57 -57
  139. package/examples/bootstrap.html +343 -332
  140. package/examples/css/baseTodo.css +141 -141
  141. package/examples/css/dbMonsterStyles.css +27 -27
  142. package/examples/css/indexTodo.css +374 -374
  143. package/examples/dbmonsterForOfReactive.html +40 -0
  144. package/examples/dbmonsterReact.html +19 -0
  145. package/examples/forOfBindingSimpleDebug.html +45 -0
  146. package/examples/form.html +20 -4
  147. package/examples/globalConfig.html +131 -0
  148. package/examples/js/afterRenderDemo.js +190 -0
  149. package/examples/js/appTodo.js +46 -46
  150. package/examples/js/attrBindingDemo.js +2 -2
  151. package/examples/js/dbMonApp.js +24 -26
  152. package/examples/js/dbMonAppReact.jsx +79 -0
  153. package/examples/js/dbMonAppReactive.js +28 -0
  154. package/examples/js/fiberDemo.js +4 -4
  155. package/examples/js/filtersDemo.js +8 -8
  156. package/examples/js/forOfDemo.js +7 -9
  157. package/examples/js/forOfDemoComplex.js +44 -17
  158. package/examples/js/form.js +44 -12
  159. package/examples/js/globalConfig.js +117 -0
  160. package/examples/js/ifBindingDemo.js +16 -16
  161. package/examples/js/reactiveDemo.js +119 -0
  162. package/examples/js/switchBindingDemo.js +8 -8
  163. package/examples/react-dbmonster/dist/bundle.js +43 -0
  164. package/examples/react-dbmonster/package-lock.json +537 -0
  165. package/examples/react-dbmonster/package.json +16 -0
  166. package/examples/react-dbmonster/src/index.jsx +80 -0
  167. package/examples/reactiveDemo.html +127 -0
  168. package/examples/refreshRateTest.html +75 -75
  169. package/index.html +841 -0
  170. package/package.json +31 -34
  171. package/rollup.config.js +79 -36
  172. package/src/{_escape.js → _escape.ts} +19 -17
  173. package/src/applyBinding.ts +179 -0
  174. package/src/{attrBinding.js → attrBinding.ts} +14 -13
  175. package/src/binder.ts +289 -0
  176. package/src/changeBinding.ts +93 -0
  177. package/src/{commentWrapper.js → commentWrapper.ts} +33 -30
  178. package/src/config.ts +107 -0
  179. package/src/createBindingOption.ts +91 -0
  180. package/src/createEventBinding.ts +88 -0
  181. package/src/{cssBinding.js → cssBinding.ts} +13 -11
  182. package/src/{domWalker.js → domWalker.ts} +44 -30
  183. package/src/{forOfBinding.js → forOfBinding.ts} +4 -3
  184. package/src/hoverBinding.ts +84 -0
  185. package/src/{ifBinding.js → ifBinding.ts} +14 -12
  186. package/src/index.ts +53 -0
  187. package/src/{modelBinding.js → modelBinding.ts} +11 -9
  188. package/src/postProcess.ts +22 -0
  189. package/src/{pubSub.js → pubSub.ts} +24 -15
  190. package/src/reactiveProxy.ts +285 -0
  191. package/src/{renderForOfBinding.js → renderForOfBinding.ts} +55 -33
  192. package/src/{renderIfBinding.js → renderIfBinding.ts} +45 -20
  193. package/src/renderIteration.ts +53 -0
  194. package/src/renderTemplate.ts +165 -0
  195. package/src/renderTemplatesBinding.ts +73 -0
  196. package/src/{showBinding.js → showBinding.ts} +4 -3
  197. package/src/{switchBinding.js → switchBinding.ts} +18 -15
  198. package/src/{textBinding.js → textBinding.ts} +5 -4
  199. package/src/types.ts +124 -0
  200. package/src/util.ts +810 -0
  201. package/test/css/reporter.css +9 -9
  202. package/test/fixtures/dataBindBootstrap.html +2 -2
  203. package/test/fixtures/formBindings.html +9 -1
  204. package/test/globals.d.ts +19 -0
  205. package/test/helpers/testHelper.js +46 -11
  206. package/test/mocks/featureAdsResult.json +65 -65
  207. package/test/mocks/searchResult.json +57 -57
  208. package/test/specs/{attrBinding.spec.js → attrBinding.spec.ts} +103 -106
  209. package/test/specs/{binder.spec.js → binder.spec.ts} +29 -27
  210. package/test/specs/blurBinding.spec.ts +60 -0
  211. package/test/specs/chainableUse.spec.ts +125 -0
  212. package/test/specs/clickBinding.spec.ts +194 -0
  213. package/test/specs/{cssBinding.spec.js → cssBinding.spec.ts} +72 -79
  214. package/test/specs/{dataBindBootstrap.spec.js → dataBindBootstrap.spec.ts} +332 -313
  215. package/test/specs/{filter.spec.js → filter.spec.ts} +75 -76
  216. package/test/specs/{forOfBinding.spec.js → forOfBinding.spec.ts} +208 -219
  217. package/test/specs/formBinding.spec.ts +272 -0
  218. package/test/specs/ifBinding.spec.ts +165 -0
  219. package/test/specs/{nestedComponent.spec.js → nestedComponent.spec.ts} +88 -88
  220. package/test/specs/reactiveProxy.spec.ts +465 -0
  221. package/test/specs/{showBinding.spec.js → showBinding.spec.ts} +148 -149
  222. package/test/specs/{switchBinding.spec.js → switchBinding.spec.ts} +172 -173
  223. package/test/specs/templateBinding.spec.ts +273 -0
  224. package/test/specs/{textBinding.spec.js → textBinding.spec.ts} +47 -48
  225. package/test/tsconfig.json +31 -0
  226. package/test-output.txt +200 -0
  227. package/test-reactive.html +224 -0
  228. package/tsconfig.json +28 -0
  229. package/vendors/lodash.custom.js +4577 -4577
  230. package/vendors/lodash.custom.min.js +45 -45
  231. package/vitest.config.js +27 -0
  232. package/.eslintrc.js +0 -1
  233. package/.grunt/grunt-contrib-jasmine/boot.js +0 -161
  234. package/.grunt/grunt-contrib-jasmine/dist/js/dataBind.js +0 -9
  235. package/.grunt/grunt-contrib-jasmine/grunt-template-jasmine-istanbul/reporter.js +0 -23
  236. package/.grunt/grunt-contrib-jasmine/jasmine-html.js +0 -853
  237. package/.grunt/grunt-contrib-jasmine/jasmine.css +0 -271
  238. package/.grunt/grunt-contrib-jasmine/jasmine.js +0 -9761
  239. package/.grunt/grunt-contrib-jasmine/jasmine_favicon.png +0 -0
  240. package/.grunt/grunt-contrib-jasmine/json2.js +0 -489
  241. package/.grunt/grunt-contrib-jasmine/reporter.js +0 -107
  242. package/coverage/coverage.json +0 -1
  243. package/coverage/lcov/lcov-report/base.css +0 -213
  244. package/coverage/lcov/lcov-report/index.html +0 -93
  245. package/coverage/lcov/lcov-report/js/dataBind.js.html +0 -6596
  246. package/coverage/lcov/lcov-report/js/index.html +0 -93
  247. package/coverage/lcov/lcov-report/prettify.css +0 -1
  248. package/coverage/lcov/lcov-report/prettify.js +0 -1
  249. package/coverage/lcov/lcov-report/sort-arrow-sprite.png +0 -0
  250. package/coverage/lcov/lcov-report/sorter.js +0 -158
  251. package/coverage/lcov/lcov.info +0 -1991
  252. package/eslintrc.json +0 -40
  253. package/examples/bootstrap/js/bootstrap.min.js +0 -6
  254. package/examples/bootstrap/js/popper.min.js +0 -5
  255. package/examples/bootstrap/js/searchSuggestion.js +0 -58
  256. package/examples/bootstrap/js/typeahead.jquery.js +0 -1538
  257. package/gruntfile.js +0 -92
  258. package/gulpfile.js +0 -32
  259. package/src/binder.js +0 -422
  260. package/src/changeBinding.js +0 -57
  261. package/src/config.js +0 -65
  262. package/src/createBindingOption.js +0 -66
  263. package/src/createEventBinding.js +0 -46
  264. package/src/eventSystem.js +0 -46
  265. package/src/hoverBinding.js +0 -57
  266. package/src/index.js +0 -26
  267. package/src/renderTemplate.js +0 -128
  268. package/src/util.js +0 -648
  269. package/test/specs/blurBinding.spec.js +0 -57
  270. package/test/specs/formBinding.spec.js +0 -292
  271. package/test/specs/ifBinding.spec.js +0 -169
  272. package/test/specs/templateBinding.spec.js +0 -117
  273. package/vendors/jasmine-jquery.js +0 -841
  274. 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
+ };