solidstep 0.1.7 → 0.2.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/README.md +47 -1
- package/client.d.ts.map +1 -1
- package/client.js +26 -0
- package/package.json +1 -1
- package/server.d.ts.map +1 -1
- package/server.js +34 -6
- package/utils/cache.d.ts +1 -0
- package/utils/cache.d.ts.map +1 -1
- package/utils/cache.js +11 -1
- package/utils/diff-dom.d.ts +72 -0
- package/utils/diff-dom.d.ts.map +1 -0
- package/utils/diff-dom.js +1081 -0
- package/utils/server-action.client.d.ts.map +1 -1
- package/utils/server-action.client.js +35 -8
- package/utils/server-action.server.d.ts.map +1 -1
- package/utils/server-action.server.js +34 -0
|
@@ -0,0 +1,1081 @@
|
|
|
1
|
+
// constants.js
|
|
2
|
+
const ACTIONS = {
|
|
3
|
+
ADD_ELEMENT: 'addElement',
|
|
4
|
+
REMOVE_ELEMENT: 'removeElement',
|
|
5
|
+
RELOCATE_ELEMENT: 'relocateElement',
|
|
6
|
+
MODIFY_TEXT: 'modifyTextElement',
|
|
7
|
+
REPLACE_ELEMENT: 'replaceElement',
|
|
8
|
+
ADD_ATTRIBUTE: 'addAttribute',
|
|
9
|
+
REMOVE_ATTRIBUTE: 'removeAttribute',
|
|
10
|
+
MODIFY_ATTRIBUTE: 'modifyAttribute',
|
|
11
|
+
MODIFY_VALUE: 'modifyValue',
|
|
12
|
+
MODIFY_CHECKED: 'modifyChecked',
|
|
13
|
+
MODIFY_SELECTED: 'modifySelected',
|
|
14
|
+
};
|
|
15
|
+
const NODE_TYPES = {
|
|
16
|
+
ELEMENT: 1,
|
|
17
|
+
TEXT: 3,
|
|
18
|
+
COMMENT: 8,
|
|
19
|
+
DOCUMENT_FRAGMENT: 11,
|
|
20
|
+
};
|
|
21
|
+
const SKIP_MODES = {
|
|
22
|
+
CHILDREN: 'children',
|
|
23
|
+
FULL: 'full',
|
|
24
|
+
};
|
|
25
|
+
const DEFAULT_OPTIONS = {
|
|
26
|
+
skipSelector: null,
|
|
27
|
+
skipPredicate: null,
|
|
28
|
+
skipAttributes: [],
|
|
29
|
+
skipChildren: false,
|
|
30
|
+
skipMode: SKIP_MODES.CHILDREN,
|
|
31
|
+
debug: false,
|
|
32
|
+
diffcap: Number.POSITIVE_INFINITY,
|
|
33
|
+
valueDiffing: true,
|
|
34
|
+
caseSensitive: false,
|
|
35
|
+
preVirtualDiffApply: null,
|
|
36
|
+
postVirtualDiffApply: null,
|
|
37
|
+
preDiffApply: null,
|
|
38
|
+
postDiffApply: null,
|
|
39
|
+
filterOuterDiff: null,
|
|
40
|
+
textDiff: null,
|
|
41
|
+
document: typeof document !== 'undefined' ? document : null,
|
|
42
|
+
};
|
|
43
|
+
const VOID_ELEMENTS = new Set([
|
|
44
|
+
'AREA',
|
|
45
|
+
'BASE',
|
|
46
|
+
'BR',
|
|
47
|
+
'COL',
|
|
48
|
+
'EMBED',
|
|
49
|
+
'HR',
|
|
50
|
+
'IMG',
|
|
51
|
+
'INPUT',
|
|
52
|
+
'LINK',
|
|
53
|
+
'META',
|
|
54
|
+
'PARAM',
|
|
55
|
+
'SOURCE',
|
|
56
|
+
'TRACK',
|
|
57
|
+
'WBR',
|
|
58
|
+
]);
|
|
59
|
+
const VOID_ELEMENTS_LOOKUP = {
|
|
60
|
+
area: true,
|
|
61
|
+
base: true,
|
|
62
|
+
br: true,
|
|
63
|
+
col: true,
|
|
64
|
+
embed: true,
|
|
65
|
+
hr: true,
|
|
66
|
+
img: true,
|
|
67
|
+
input: true,
|
|
68
|
+
keygen: true,
|
|
69
|
+
link: true,
|
|
70
|
+
menuitem: true,
|
|
71
|
+
meta: true,
|
|
72
|
+
param: true,
|
|
73
|
+
source: true,
|
|
74
|
+
track: true,
|
|
75
|
+
wbr: true,
|
|
76
|
+
};
|
|
77
|
+
const tagRE = /<\s*\/*[a-zA-Z:_][a-zA-Z0-9:_\-.]*\s*(?:"[^"]*"['"]*|'[^']*'['"]*|[^'"/>])*\/*\s*>|<!--(?:.|\n|\r)*?-->/g;
|
|
78
|
+
const attrRE = /\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?("[^"]*"|'[^']*')/g;
|
|
79
|
+
const unescapeHTML = (string) => {
|
|
80
|
+
return string
|
|
81
|
+
.replace(/</g, '<')
|
|
82
|
+
.replace(/>/g, '>')
|
|
83
|
+
.replace(/&/g, '&');
|
|
84
|
+
};
|
|
85
|
+
const parseTag = (tag, caseSensitive) => {
|
|
86
|
+
const res = {
|
|
87
|
+
nodeType: NODE_TYPES.ELEMENT,
|
|
88
|
+
nodeName: '',
|
|
89
|
+
attributes: {},
|
|
90
|
+
};
|
|
91
|
+
let voidElement = false;
|
|
92
|
+
const type = 'tag';
|
|
93
|
+
const tagMatch = tag.match(/<\/?([^\s]+?)[/\s>]/);
|
|
94
|
+
if (tagMatch) {
|
|
95
|
+
res.nodeName =
|
|
96
|
+
caseSensitive || tagMatch[1] === 'svg'
|
|
97
|
+
? tagMatch[1]
|
|
98
|
+
: tagMatch[1].toUpperCase();
|
|
99
|
+
if (VOID_ELEMENTS_LOOKUP[tagMatch[1].toLowerCase()] ||
|
|
100
|
+
tag.charAt(tag.length - 2) === '/') {
|
|
101
|
+
voidElement = true;
|
|
102
|
+
}
|
|
103
|
+
if (res.nodeName.startsWith('!--')) {
|
|
104
|
+
const endIndex = tag.indexOf('-->');
|
|
105
|
+
return {
|
|
106
|
+
type: 'comment',
|
|
107
|
+
node: {
|
|
108
|
+
nodeName: '#comment',
|
|
109
|
+
nodeType: NODE_TYPES.COMMENT,
|
|
110
|
+
data: endIndex !== -1 ? tag.slice(4, endIndex) : '',
|
|
111
|
+
},
|
|
112
|
+
voidElement,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const reg = new RegExp(attrRE);
|
|
117
|
+
let result = null;
|
|
118
|
+
let done = false;
|
|
119
|
+
while (!done) {
|
|
120
|
+
result = reg.exec(tag);
|
|
121
|
+
if (result === null) {
|
|
122
|
+
done = true;
|
|
123
|
+
}
|
|
124
|
+
else if (result[0].trim()) {
|
|
125
|
+
if (result[1]) {
|
|
126
|
+
const attr = result[1].trim();
|
|
127
|
+
let arr = [attr, ''];
|
|
128
|
+
if (attr.indexOf('=') > -1)
|
|
129
|
+
arr = attr.split('=');
|
|
130
|
+
if (!res.attributes)
|
|
131
|
+
res.attributes = {};
|
|
132
|
+
res.attributes[arr[0]] = arr[1];
|
|
133
|
+
reg.lastIndex--;
|
|
134
|
+
}
|
|
135
|
+
else if (result[2]) {
|
|
136
|
+
if (!res.attributes)
|
|
137
|
+
res.attributes = {};
|
|
138
|
+
res.attributes[result[2]] = result[3]
|
|
139
|
+
.trim()
|
|
140
|
+
.substring(1, result[3].length - 1);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
type,
|
|
146
|
+
node: res,
|
|
147
|
+
voidElement,
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
const getNodeByRoute = (root, route) => {
|
|
151
|
+
let node = root;
|
|
152
|
+
for (const index of route) {
|
|
153
|
+
if (!node.childNodes || !node.childNodes[index]) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
node = node.childNodes[index];
|
|
157
|
+
}
|
|
158
|
+
return node;
|
|
159
|
+
};
|
|
160
|
+
const cloneRoute = (route) => [...route];
|
|
161
|
+
const normalizeNodeName = (nodeName, caseSensitive) => {
|
|
162
|
+
return caseSensitive ? nodeName : nodeName.toUpperCase();
|
|
163
|
+
};
|
|
164
|
+
const isVoidElement = (nodeName) => {
|
|
165
|
+
return VOID_ELEMENTS.has(nodeName.toUpperCase());
|
|
166
|
+
};
|
|
167
|
+
const getElementKey = (node) => {
|
|
168
|
+
if (node.nodeType !== NODE_TYPES.ELEMENT)
|
|
169
|
+
return null;
|
|
170
|
+
return node.attributes?.['data-key'] || node.attributes?.id || null;
|
|
171
|
+
};
|
|
172
|
+
const elementsMatch = (nodeA, nodeB, options) => {
|
|
173
|
+
if (nodeA.nodeType !== nodeB.nodeType)
|
|
174
|
+
return false;
|
|
175
|
+
if (nodeA.nodeType === NODE_TYPES.TEXT ||
|
|
176
|
+
nodeA.nodeType === NODE_TYPES.COMMENT) {
|
|
177
|
+
return nodeA.data === nodeB.data;
|
|
178
|
+
}
|
|
179
|
+
if (nodeA.nodeType === NODE_TYPES.ELEMENT) {
|
|
180
|
+
const nameA = normalizeNodeName(nodeA.nodeName, options.caseSensitive);
|
|
181
|
+
const nameB = normalizeNodeName(nodeB.nodeName, options.caseSensitive);
|
|
182
|
+
return nameA === nameB;
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
};
|
|
186
|
+
const calculateSimilarity = (nodeA, nodeB, options) => {
|
|
187
|
+
if (!elementsMatch(nodeA, nodeB, options))
|
|
188
|
+
return 0;
|
|
189
|
+
if (nodeA.nodeType !== NODE_TYPES.ELEMENT)
|
|
190
|
+
return 1;
|
|
191
|
+
const attrsA = nodeA.attributes || {};
|
|
192
|
+
const attrsB = nodeB.attributes || {};
|
|
193
|
+
const allKeys = new Set([...Object.keys(attrsA), ...Object.keys(attrsB)]);
|
|
194
|
+
if (allKeys.size === 0)
|
|
195
|
+
return 1;
|
|
196
|
+
let matches = 0;
|
|
197
|
+
for (const key of allKeys) {
|
|
198
|
+
if (attrsA[key] === attrsB[key])
|
|
199
|
+
matches++;
|
|
200
|
+
}
|
|
201
|
+
return matches / allKeys.size;
|
|
202
|
+
};
|
|
203
|
+
export const nodeToObj = (node, options = {}) => {
|
|
204
|
+
if (!node)
|
|
205
|
+
return null;
|
|
206
|
+
const obj = {
|
|
207
|
+
nodeType: node.nodeType,
|
|
208
|
+
nodeName: '',
|
|
209
|
+
route: [],
|
|
210
|
+
};
|
|
211
|
+
if (node.nodeType === NODE_TYPES.TEXT) {
|
|
212
|
+
obj.nodeName = '#text';
|
|
213
|
+
obj.data = node.data || node.textContent || '';
|
|
214
|
+
}
|
|
215
|
+
else if (node.nodeType === NODE_TYPES.COMMENT) {
|
|
216
|
+
obj.nodeName = '#comment';
|
|
217
|
+
obj.data = node.data || '';
|
|
218
|
+
}
|
|
219
|
+
else if (node.nodeType === NODE_TYPES.ELEMENT) {
|
|
220
|
+
obj.nodeName = normalizeNodeName(node.nodeName, options.caseSensitive ?? false);
|
|
221
|
+
obj.attributes = {};
|
|
222
|
+
const element = node;
|
|
223
|
+
if (element.attributes) {
|
|
224
|
+
for (let i = 0; i < element.attributes.length; i++) {
|
|
225
|
+
const attr = element.attributes[i];
|
|
226
|
+
obj.attributes[attr.name] = attr.value;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (options.valueDiffing !== false) {
|
|
230
|
+
const htmlElement = element;
|
|
231
|
+
if (htmlElement.value !== undefined &&
|
|
232
|
+
['INPUT', 'TEXTAREA', 'SELECT'].includes(obj.nodeName)) {
|
|
233
|
+
obj.value = htmlElement.value;
|
|
234
|
+
}
|
|
235
|
+
if (htmlElement.checked !==
|
|
236
|
+
undefined) {
|
|
237
|
+
obj.checked = htmlElement.checked;
|
|
238
|
+
}
|
|
239
|
+
if (htmlElement.selected !==
|
|
240
|
+
undefined) {
|
|
241
|
+
obj.selected = htmlElement.selected;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (!isVoidElement(obj.nodeName) && node.childNodes) {
|
|
245
|
+
obj.childNodes = [];
|
|
246
|
+
for (let i = 0; i < node.childNodes.length; i++) {
|
|
247
|
+
const childObj = nodeToObj(node.childNodes[i], options);
|
|
248
|
+
if (childObj) {
|
|
249
|
+
childObj.route = [...(obj.route || []), i];
|
|
250
|
+
obj.childNodes.push(childObj);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
else if (node.nodeType === NODE_TYPES.DOCUMENT_FRAGMENT) {
|
|
256
|
+
obj.nodeName = '#document-fragment';
|
|
257
|
+
obj.childNodes = [];
|
|
258
|
+
if (node.childNodes) {
|
|
259
|
+
for (let i = 0; i < node.childNodes.length; i++) {
|
|
260
|
+
const childObj = nodeToObj(node.childNodes[i], options);
|
|
261
|
+
if (childObj) {
|
|
262
|
+
childObj.route = [i];
|
|
263
|
+
obj.childNodes.push(childObj);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return obj;
|
|
269
|
+
};
|
|
270
|
+
export const stringToObj = (htmlString, options = {}) => {
|
|
271
|
+
const result = [];
|
|
272
|
+
let current = null;
|
|
273
|
+
let level = -1;
|
|
274
|
+
const arr = [];
|
|
275
|
+
let inComponent = false;
|
|
276
|
+
let insideSvg = false;
|
|
277
|
+
const caseSensitive = options.caseSensitive || false;
|
|
278
|
+
const valueDiffing = options.valueDiffing !== false;
|
|
279
|
+
if (htmlString.indexOf('<') !== 0) {
|
|
280
|
+
const end = htmlString.indexOf('<');
|
|
281
|
+
result.push({
|
|
282
|
+
nodeType: NODE_TYPES.TEXT,
|
|
283
|
+
nodeName: '#text',
|
|
284
|
+
data: end === -1 ? htmlString : htmlString.substring(0, end),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
htmlString.replace(tagRE, (tag, index) => {
|
|
288
|
+
if (inComponent) {
|
|
289
|
+
if (tag !== `</${current.node.nodeName}>`) {
|
|
290
|
+
return '';
|
|
291
|
+
}
|
|
292
|
+
inComponent = false;
|
|
293
|
+
}
|
|
294
|
+
const isOpen = tag.charAt(1) !== '/';
|
|
295
|
+
const isComment = tag.startsWith('<!--');
|
|
296
|
+
const start = index + tag.length;
|
|
297
|
+
const nextChar = htmlString.charAt(start);
|
|
298
|
+
if (isComment) {
|
|
299
|
+
const comment = parseTag(tag, caseSensitive).node;
|
|
300
|
+
if (level < 0) {
|
|
301
|
+
result.push(comment);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
const parent = arr[level];
|
|
305
|
+
if (parent && comment.nodeName) {
|
|
306
|
+
if (!parent.node.childNodes) {
|
|
307
|
+
parent.node.childNodes = [];
|
|
308
|
+
}
|
|
309
|
+
parent.node.childNodes.push(comment);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (!inComponent && nextChar !== '<' && nextChar) {
|
|
313
|
+
const childNodes = level === -1 ? result : arr[level]?.node.childNodes || [];
|
|
314
|
+
const end = htmlString.indexOf('<', start);
|
|
315
|
+
const data = unescapeHTML(htmlString.slice(start, end === -1 ? undefined : end));
|
|
316
|
+
if (data) {
|
|
317
|
+
childNodes.push({
|
|
318
|
+
nodeType: NODE_TYPES.TEXT,
|
|
319
|
+
nodeName: '#text',
|
|
320
|
+
data,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return '';
|
|
325
|
+
}
|
|
326
|
+
if (isOpen) {
|
|
327
|
+
current = parseTag(tag, caseSensitive || insideSvg);
|
|
328
|
+
current.node.nodeType = NODE_TYPES.ELEMENT;
|
|
329
|
+
if (current.node.nodeName === 'SVG' ||
|
|
330
|
+
current.node.nodeName === 'svg') {
|
|
331
|
+
insideSvg = true;
|
|
332
|
+
}
|
|
333
|
+
level++;
|
|
334
|
+
if (!current.voidElement &&
|
|
335
|
+
!inComponent &&
|
|
336
|
+
nextChar &&
|
|
337
|
+
nextChar !== '<') {
|
|
338
|
+
if (!current.node.childNodes) {
|
|
339
|
+
current.node.childNodes = [];
|
|
340
|
+
}
|
|
341
|
+
const endIndex = htmlString.indexOf('<', start);
|
|
342
|
+
const data = unescapeHTML(htmlString.slice(start, endIndex === -1 ? undefined : endIndex));
|
|
343
|
+
current.node.childNodes.push({
|
|
344
|
+
nodeType: NODE_TYPES.TEXT,
|
|
345
|
+
nodeName: '#text',
|
|
346
|
+
data,
|
|
347
|
+
});
|
|
348
|
+
if (valueDiffing && current.node.nodeName === 'TEXTAREA') {
|
|
349
|
+
current.node.value = data;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (level === 0 && current.node.nodeName) {
|
|
353
|
+
result.push(current.node);
|
|
354
|
+
}
|
|
355
|
+
const parent = arr[level - 1];
|
|
356
|
+
if (parent && current.node.nodeName) {
|
|
357
|
+
if (!parent.node.childNodes) {
|
|
358
|
+
parent.node.childNodes = [];
|
|
359
|
+
}
|
|
360
|
+
parent.node.childNodes.push(current.node);
|
|
361
|
+
}
|
|
362
|
+
arr[level] = current;
|
|
363
|
+
}
|
|
364
|
+
if (!isOpen || current?.voidElement) {
|
|
365
|
+
if (level > -1 &&
|
|
366
|
+
current &&
|
|
367
|
+
(current.voidElement ||
|
|
368
|
+
(caseSensitive &&
|
|
369
|
+
current.node.nodeName === tag.slice(2, -1)) ||
|
|
370
|
+
(!caseSensitive &&
|
|
371
|
+
current.node.nodeName.toUpperCase() ===
|
|
372
|
+
tag.slice(2, -1).toUpperCase()))) {
|
|
373
|
+
level--;
|
|
374
|
+
if (level > -1) {
|
|
375
|
+
if (current.node.nodeName === 'SVG' ||
|
|
376
|
+
current.node.nodeName === 'svg') {
|
|
377
|
+
insideSvg = false;
|
|
378
|
+
}
|
|
379
|
+
current = arr[level];
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (!inComponent && nextChar !== '<' && nextChar) {
|
|
383
|
+
const childNodes = level === -1 ? result : arr[level]?.node.childNodes || [];
|
|
384
|
+
const end = htmlString.indexOf('<', start);
|
|
385
|
+
const data = unescapeHTML(htmlString.slice(start, end === -1 ? undefined : end));
|
|
386
|
+
childNodes.push({
|
|
387
|
+
nodeType: NODE_TYPES.TEXT,
|
|
388
|
+
nodeName: '#text',
|
|
389
|
+
data,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return '';
|
|
394
|
+
});
|
|
395
|
+
if (result.length === 1) {
|
|
396
|
+
return result[0];
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
nodeType: NODE_TYPES.DOCUMENT_FRAGMENT,
|
|
400
|
+
nodeName: '#document-fragment',
|
|
401
|
+
childNodes: result,
|
|
402
|
+
route: [],
|
|
403
|
+
};
|
|
404
|
+
};
|
|
405
|
+
export const objToNode = (obj, options = {}) => {
|
|
406
|
+
const doc = options.document || DEFAULT_OPTIONS.document;
|
|
407
|
+
if (!doc) {
|
|
408
|
+
throw new Error('Document object is required for objToNode');
|
|
409
|
+
}
|
|
410
|
+
if (obj.nodeType === NODE_TYPES.TEXT) {
|
|
411
|
+
return doc.createTextNode(obj.data || '');
|
|
412
|
+
}
|
|
413
|
+
if (obj.nodeType === NODE_TYPES.COMMENT) {
|
|
414
|
+
return doc.createComment(obj.data || '');
|
|
415
|
+
}
|
|
416
|
+
if (obj.nodeType === NODE_TYPES.ELEMENT) {
|
|
417
|
+
const element = doc.createElement(obj.nodeName);
|
|
418
|
+
if (obj.attributes) {
|
|
419
|
+
for (const key of Object.keys(obj.attributes)) {
|
|
420
|
+
element.setAttribute(key, obj.attributes[key]);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (obj.value !== undefined) {
|
|
424
|
+
element.value = obj.value;
|
|
425
|
+
}
|
|
426
|
+
if (obj.checked !== undefined) {
|
|
427
|
+
element.checked = obj.checked;
|
|
428
|
+
}
|
|
429
|
+
if (obj.selected !== undefined) {
|
|
430
|
+
element.selected = obj.selected;
|
|
431
|
+
}
|
|
432
|
+
if (obj.childNodes) {
|
|
433
|
+
for (const childObj of obj.childNodes) {
|
|
434
|
+
const childNode = objToNode(childObj, options);
|
|
435
|
+
if (childNode) {
|
|
436
|
+
element.appendChild(childNode);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return element;
|
|
441
|
+
}
|
|
442
|
+
if (obj.nodeType === NODE_TYPES.DOCUMENT_FRAGMENT) {
|
|
443
|
+
const fragment = doc.createDocumentFragment();
|
|
444
|
+
if (obj.childNodes) {
|
|
445
|
+
for (const childObj of obj.childNodes) {
|
|
446
|
+
const childNode = objToNode(childObj, options);
|
|
447
|
+
if (childNode) {
|
|
448
|
+
fragment.appendChild(childNode);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return fragment;
|
|
453
|
+
}
|
|
454
|
+
return null;
|
|
455
|
+
};
|
|
456
|
+
const matchesSimpleSelector = (node, selector) => {
|
|
457
|
+
if (!selector || node.nodeType !== NODE_TYPES.ELEMENT)
|
|
458
|
+
return false;
|
|
459
|
+
const trimmedSelector = selector.trim();
|
|
460
|
+
// Tag selector (e.g., "div", "span")
|
|
461
|
+
if (/^[a-zA-Z][\w-]*$/.test(trimmedSelector)) {
|
|
462
|
+
return (normalizeNodeName(node.nodeName, false) ===
|
|
463
|
+
trimmedSelector.toUpperCase());
|
|
464
|
+
}
|
|
465
|
+
// ID selector (e.g., "#myId")
|
|
466
|
+
if (trimmedSelector.startsWith('#')) {
|
|
467
|
+
const id = trimmedSelector.slice(1);
|
|
468
|
+
return node.attributes?.id === id;
|
|
469
|
+
}
|
|
470
|
+
// Class selector (e.g., ".myClass")
|
|
471
|
+
if (trimmedSelector.startsWith('.')) {
|
|
472
|
+
const className = trimmedSelector.slice(1);
|
|
473
|
+
const nodeClasses = node.attributes?.class?.split(/\s+/) || [];
|
|
474
|
+
return nodeClasses.includes(className);
|
|
475
|
+
}
|
|
476
|
+
// Attribute selector (e.g., "[data-skip]", "[type='text']")
|
|
477
|
+
const attrMatch = trimmedSelector.match(/^\[([^\]=]+)(?:=["']?([^"'\]]+)["']?)?\]$/);
|
|
478
|
+
if (attrMatch) {
|
|
479
|
+
const [, attrName, attrValue] = attrMatch;
|
|
480
|
+
if (attrValue !== undefined) {
|
|
481
|
+
return node.attributes?.[attrName] === attrValue;
|
|
482
|
+
}
|
|
483
|
+
return node.attributes?.[attrName] !== undefined;
|
|
484
|
+
}
|
|
485
|
+
// Tag with class (e.g., "div.myClass")
|
|
486
|
+
const tagClassMatch = trimmedSelector.match(/^([a-zA-Z][\w-]*)\.([a-zA-Z][\w-]*)$/);
|
|
487
|
+
if (tagClassMatch) {
|
|
488
|
+
const [, tag, className] = tagClassMatch;
|
|
489
|
+
const nodeClasses = node.attributes?.class?.split(/\s+/) || [];
|
|
490
|
+
return (normalizeNodeName(node.nodeName, false) === tag.toUpperCase() &&
|
|
491
|
+
nodeClasses.includes(className));
|
|
492
|
+
}
|
|
493
|
+
// Tag with ID (e.g., "div#myId")
|
|
494
|
+
const tagIdMatch = trimmedSelector.match(/^([a-zA-Z][\w-]*)#([a-zA-Z][\w-]*)$/);
|
|
495
|
+
if (tagIdMatch) {
|
|
496
|
+
const [, tag, id] = tagIdMatch;
|
|
497
|
+
return (normalizeNodeName(node.nodeName, false) === tag.toUpperCase() &&
|
|
498
|
+
node.attributes?.id === id);
|
|
499
|
+
}
|
|
500
|
+
return false;
|
|
501
|
+
};
|
|
502
|
+
const matchesSelector = (node, selector) => {
|
|
503
|
+
if (!selector)
|
|
504
|
+
return false;
|
|
505
|
+
// Handle multiple selectors separated by comma
|
|
506
|
+
const selectors = selector.split(',').map((s) => s.trim());
|
|
507
|
+
return selectors.some((sel) => matchesSimpleSelector(node, sel));
|
|
508
|
+
};
|
|
509
|
+
const shouldSkipElement = (node, domNode, options) => {
|
|
510
|
+
if (node.nodeType !== NODE_TYPES.ELEMENT)
|
|
511
|
+
return false;
|
|
512
|
+
// Check CSS selector - try DOM API first, fall back to virtual matching
|
|
513
|
+
if (options.skipSelector) {
|
|
514
|
+
let matches = false;
|
|
515
|
+
// Try browser DOM API if available
|
|
516
|
+
if (domNode &&
|
|
517
|
+
'matches' in domNode &&
|
|
518
|
+
typeof domNode.matches === 'function') {
|
|
519
|
+
matches = domNode.matches(options.skipSelector);
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
// Fall back to virtual node matching for Node.js
|
|
523
|
+
matches = matchesSelector(node, options.skipSelector);
|
|
524
|
+
}
|
|
525
|
+
if (matches) {
|
|
526
|
+
return options.skipMode || SKIP_MODES.CHILDREN;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
// Check custom predicate - pass both domNode and virtual node
|
|
530
|
+
if (options.skipPredicate) {
|
|
531
|
+
const result = options.skipPredicate(domNode || node, node);
|
|
532
|
+
if (result === true) {
|
|
533
|
+
return options.skipMode || SKIP_MODES.CHILDREN;
|
|
534
|
+
}
|
|
535
|
+
if (result === SKIP_MODES.CHILDREN || result === SKIP_MODES.FULL) {
|
|
536
|
+
return result;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return false;
|
|
540
|
+
};
|
|
541
|
+
const applySkipMode = (node, skipMode) => {
|
|
542
|
+
if (skipMode === SKIP_MODES.CHILDREN) {
|
|
543
|
+
node.innerDone = true;
|
|
544
|
+
}
|
|
545
|
+
else if (skipMode === SKIP_MODES.FULL) {
|
|
546
|
+
node.skipFull = true;
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
const diffAttributes = (oldAttrs, newAttrs, route, options) => {
|
|
550
|
+
const diffs = [];
|
|
551
|
+
const allKeys = new Set([
|
|
552
|
+
...Object.keys(oldAttrs || {}),
|
|
553
|
+
...Object.keys(newAttrs || {}),
|
|
554
|
+
]);
|
|
555
|
+
for (const key of allKeys) {
|
|
556
|
+
if (options.skipAttributes.includes(key))
|
|
557
|
+
continue;
|
|
558
|
+
const oldVal = oldAttrs?.[key];
|
|
559
|
+
const newVal = newAttrs?.[key];
|
|
560
|
+
if (oldVal === undefined && newVal !== undefined) {
|
|
561
|
+
diffs.push({
|
|
562
|
+
action: ACTIONS.ADD_ATTRIBUTE,
|
|
563
|
+
route: cloneRoute(route),
|
|
564
|
+
name: key,
|
|
565
|
+
value: newVal,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
else if (oldVal !== undefined && newVal === undefined) {
|
|
569
|
+
diffs.push({
|
|
570
|
+
action: ACTIONS.REMOVE_ATTRIBUTE,
|
|
571
|
+
route: cloneRoute(route),
|
|
572
|
+
name: key,
|
|
573
|
+
value: oldVal,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
else if (oldVal !== newVal) {
|
|
577
|
+
diffs.push({
|
|
578
|
+
action: ACTIONS.MODIFY_ATTRIBUTE,
|
|
579
|
+
route: cloneRoute(route),
|
|
580
|
+
name: key,
|
|
581
|
+
oldValue: oldVal,
|
|
582
|
+
newValue: newVal,
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return diffs;
|
|
587
|
+
};
|
|
588
|
+
const findBestMatch = (oldChild, newChildren, startIndex, options) => {
|
|
589
|
+
let bestIndex = -1;
|
|
590
|
+
let bestScore = 0;
|
|
591
|
+
const oldKey = getElementKey(oldChild);
|
|
592
|
+
for (let i = startIndex; i < newChildren.length; i++) {
|
|
593
|
+
const newChild = newChildren[i];
|
|
594
|
+
// Check for key match
|
|
595
|
+
if (oldKey && oldKey === getElementKey(newChild)) {
|
|
596
|
+
return { index: i, score: 1, keyMatch: true };
|
|
597
|
+
}
|
|
598
|
+
// Calculate similarity
|
|
599
|
+
const score = calculateSimilarity(oldChild, newChild, options);
|
|
600
|
+
if (score > bestScore) {
|
|
601
|
+
bestScore = score;
|
|
602
|
+
bestIndex = i;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return bestScore > 0.5
|
|
606
|
+
? { index: bestIndex, score: bestScore, keyMatch: false }
|
|
607
|
+
: null;
|
|
608
|
+
};
|
|
609
|
+
const diffChildren = (oldChildrenP, newChildrenP, route, options, diffCount) => {
|
|
610
|
+
let oldChildren = oldChildrenP;
|
|
611
|
+
let newChildren = newChildrenP;
|
|
612
|
+
if (!oldChildren)
|
|
613
|
+
oldChildren = [];
|
|
614
|
+
if (!newChildren)
|
|
615
|
+
newChildren = [];
|
|
616
|
+
const diffs = [];
|
|
617
|
+
const oldUsed = new Set();
|
|
618
|
+
const newUsed = new Set();
|
|
619
|
+
const matches = [];
|
|
620
|
+
// First pass: find matches
|
|
621
|
+
for (let i = 0; i < oldChildren.length; i++) {
|
|
622
|
+
const oldChild = oldChildren[i];
|
|
623
|
+
// Try exact position match first
|
|
624
|
+
if (i < newChildren.length && !newUsed.has(i)) {
|
|
625
|
+
if (elementsMatch(oldChild, newChildren[i], options)) {
|
|
626
|
+
const similarity = calculateSimilarity(oldChild, newChildren[i], options);
|
|
627
|
+
if (similarity > 0.7) {
|
|
628
|
+
matches.push({
|
|
629
|
+
oldIndex: i,
|
|
630
|
+
newIndex: i,
|
|
631
|
+
score: similarity,
|
|
632
|
+
});
|
|
633
|
+
oldUsed.add(i);
|
|
634
|
+
newUsed.add(i);
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
// Find best match in remaining new children
|
|
640
|
+
const match = findBestMatch(oldChild, newChildren, 0, options);
|
|
641
|
+
if (match && !newUsed.has(match.index)) {
|
|
642
|
+
matches.push({
|
|
643
|
+
oldIndex: i,
|
|
644
|
+
newIndex: match.index,
|
|
645
|
+
score: match.score,
|
|
646
|
+
});
|
|
647
|
+
oldUsed.add(i);
|
|
648
|
+
newUsed.add(match.index);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
// Sort matches by old index to process in order
|
|
652
|
+
matches.sort((a, b) => a.oldIndex - b.oldIndex);
|
|
653
|
+
// Second pass: detect relocations and recurse on matches
|
|
654
|
+
for (const match of matches) {
|
|
655
|
+
const oldChild = oldChildren[match.oldIndex];
|
|
656
|
+
const newChild = newChildren[match.newIndex];
|
|
657
|
+
// Check if relocation is needed
|
|
658
|
+
if (match.oldIndex !== match.newIndex) {
|
|
659
|
+
diffs.push({
|
|
660
|
+
action: ACTIONS.RELOCATE_ELEMENT,
|
|
661
|
+
from: [...route, match.oldIndex],
|
|
662
|
+
to: [...route, match.newIndex],
|
|
663
|
+
route: cloneRoute(route),
|
|
664
|
+
});
|
|
665
|
+
if (options.debug && diffs.length >= diffCount.value) {
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
// Recurse on matched elements
|
|
670
|
+
const childRoute = [...route, match.oldIndex];
|
|
671
|
+
const childDiffs = diffNode(oldChild, newChild, childRoute, options, diffCount);
|
|
672
|
+
diffs.push(...childDiffs);
|
|
673
|
+
}
|
|
674
|
+
// Third pass: handle removes (old children not matched)
|
|
675
|
+
for (let i = oldChildren.length - 1; i >= 0; i--) {
|
|
676
|
+
if (!oldUsed.has(i)) {
|
|
677
|
+
diffs.push({
|
|
678
|
+
action: ACTIONS.REMOVE_ELEMENT,
|
|
679
|
+
route: [...route, i],
|
|
680
|
+
element: oldChildren[i],
|
|
681
|
+
});
|
|
682
|
+
if (options.debug && diffs.length >= diffCount.value) {
|
|
683
|
+
return diffs;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
// Fourth pass: handle adds (new children not matched)
|
|
688
|
+
for (let i = 0; i < newChildren.length; i++) {
|
|
689
|
+
if (!newUsed.has(i)) {
|
|
690
|
+
diffs.push({
|
|
691
|
+
action: ACTIONS.ADD_ELEMENT,
|
|
692
|
+
route: cloneRoute(route),
|
|
693
|
+
element: newChildren[i],
|
|
694
|
+
index: i,
|
|
695
|
+
});
|
|
696
|
+
if (options.debug && diffs.length >= diffCount.value) {
|
|
697
|
+
return diffs;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return diffs;
|
|
702
|
+
};
|
|
703
|
+
const diffNode = (oldNode, newNode, route, options, diffCount) => {
|
|
704
|
+
const diffs = [];
|
|
705
|
+
// Check if we've hit the diff cap
|
|
706
|
+
if (options.debug && diffCount.value >= options.diffcap) {
|
|
707
|
+
return diffs;
|
|
708
|
+
}
|
|
709
|
+
// Skip if marked as full skip
|
|
710
|
+
if (oldNode.skipFull || newNode.skipFull) {
|
|
711
|
+
return diffs;
|
|
712
|
+
}
|
|
713
|
+
// Different node types or names - replace entire node
|
|
714
|
+
if (!elementsMatch(oldNode, newNode, options)) {
|
|
715
|
+
diffs.push({
|
|
716
|
+
action: ACTIONS.REPLACE_ELEMENT,
|
|
717
|
+
route: cloneRoute(route),
|
|
718
|
+
oldValue: oldNode,
|
|
719
|
+
newValue: newNode,
|
|
720
|
+
});
|
|
721
|
+
diffCount.value += diffs.length;
|
|
722
|
+
return diffs;
|
|
723
|
+
}
|
|
724
|
+
// Text nodes
|
|
725
|
+
if (oldNode.nodeType === NODE_TYPES.TEXT) {
|
|
726
|
+
if (oldNode.data !== newNode.data) {
|
|
727
|
+
diffs.push({
|
|
728
|
+
action: ACTIONS.MODIFY_TEXT,
|
|
729
|
+
route: cloneRoute(route),
|
|
730
|
+
oldValue: oldNode.data,
|
|
731
|
+
newValue: newNode.data,
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
diffCount.value += diffs.length;
|
|
735
|
+
return diffs;
|
|
736
|
+
}
|
|
737
|
+
// Comment nodes
|
|
738
|
+
if (oldNode.nodeType === NODE_TYPES.COMMENT) {
|
|
739
|
+
if (oldNode.data !== newNode.data) {
|
|
740
|
+
diffs.push({
|
|
741
|
+
action: ACTIONS.MODIFY_TEXT,
|
|
742
|
+
route: cloneRoute(route),
|
|
743
|
+
oldValue: oldNode.data,
|
|
744
|
+
newValue: newNode.data,
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
diffCount.value += diffs.length;
|
|
748
|
+
return diffs;
|
|
749
|
+
}
|
|
750
|
+
// Element nodes
|
|
751
|
+
if (oldNode.nodeType === NODE_TYPES.ELEMENT) {
|
|
752
|
+
// Diff attributes
|
|
753
|
+
const attrDiffs = diffAttributes(oldNode.attributes, newNode.attributes, route, options);
|
|
754
|
+
diffs.push(...attrDiffs);
|
|
755
|
+
// Diff form values
|
|
756
|
+
if (options.valueDiffing) {
|
|
757
|
+
if (oldNode.value !== newNode.value &&
|
|
758
|
+
newNode.value !== undefined) {
|
|
759
|
+
diffs.push({
|
|
760
|
+
action: ACTIONS.MODIFY_VALUE,
|
|
761
|
+
route: cloneRoute(route),
|
|
762
|
+
oldValue: oldNode.value,
|
|
763
|
+
newValue: newNode.value,
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
if (oldNode.checked !== newNode.checked &&
|
|
767
|
+
newNode.checked !== undefined) {
|
|
768
|
+
diffs.push({
|
|
769
|
+
action: ACTIONS.MODIFY_CHECKED,
|
|
770
|
+
route: cloneRoute(route),
|
|
771
|
+
oldValue: oldNode.checked,
|
|
772
|
+
newValue: newNode.checked,
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
if (oldNode.selected !== newNode.selected &&
|
|
776
|
+
newNode.selected !== undefined) {
|
|
777
|
+
diffs.push({
|
|
778
|
+
action: ACTIONS.MODIFY_SELECTED,
|
|
779
|
+
route: cloneRoute(route),
|
|
780
|
+
oldValue: oldNode.selected,
|
|
781
|
+
newValue: newNode.selected,
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
diffCount.value += diffs.length;
|
|
786
|
+
if (options.debug && diffCount.value >= options.diffcap) {
|
|
787
|
+
return diffs;
|
|
788
|
+
}
|
|
789
|
+
// Check if we should skip children
|
|
790
|
+
if (oldNode.innerDone || newNode.innerDone || options.skipChildren) {
|
|
791
|
+
return diffs;
|
|
792
|
+
}
|
|
793
|
+
// Diff children
|
|
794
|
+
const childDiffs = diffChildren(oldNode.childNodes, newNode.childNodes, route, options, diffCount);
|
|
795
|
+
diffs.push(...childDiffs);
|
|
796
|
+
}
|
|
797
|
+
diffCount.value += diffs.length;
|
|
798
|
+
return diffs;
|
|
799
|
+
};
|
|
800
|
+
export const diff = (elementA, elementB, options = {}) => {
|
|
801
|
+
const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
|
|
802
|
+
let objA;
|
|
803
|
+
let objB;
|
|
804
|
+
if (typeof elementA === 'string') {
|
|
805
|
+
objA = stringToObj(elementA, mergedOptions);
|
|
806
|
+
}
|
|
807
|
+
else if ('nodeType' in elementA &&
|
|
808
|
+
typeof elementA.nodeType === 'number') {
|
|
809
|
+
objA = nodeToObj(elementA, mergedOptions);
|
|
810
|
+
}
|
|
811
|
+
else {
|
|
812
|
+
objA = elementA;
|
|
813
|
+
}
|
|
814
|
+
if (typeof elementB === 'string') {
|
|
815
|
+
objB = stringToObj(elementB, mergedOptions);
|
|
816
|
+
}
|
|
817
|
+
else if ('nodeType' in elementB &&
|
|
818
|
+
typeof elementB.nodeType === 'number') {
|
|
819
|
+
objB = nodeToObj(elementB, mergedOptions);
|
|
820
|
+
}
|
|
821
|
+
else {
|
|
822
|
+
objB = elementB;
|
|
823
|
+
}
|
|
824
|
+
// Apply skip logic to both trees
|
|
825
|
+
const applySkipLogic = (node, domNode) => {
|
|
826
|
+
if (node.nodeType === NODE_TYPES.ELEMENT) {
|
|
827
|
+
const skipMode = shouldSkipElement(node, domNode, mergedOptions);
|
|
828
|
+
if (skipMode) {
|
|
829
|
+
applySkipMode(node, skipMode);
|
|
830
|
+
}
|
|
831
|
+
// Recurse on children
|
|
832
|
+
if (node.childNodes && !node.skipFull) {
|
|
833
|
+
node.childNodes.forEach((child, i) => {
|
|
834
|
+
const childDom = domNode?.childNodes?.[i];
|
|
835
|
+
applySkipLogic(child, childDom || null);
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
else if (node.nodeType === NODE_TYPES.COMMENT) {
|
|
840
|
+
applySkipMode(node, SKIP_MODES.FULL);
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
// Get domNode reference if available (for browser DOM nodes)
|
|
844
|
+
const domNodeA = typeof elementA === 'object' &&
|
|
845
|
+
'nodeType' in elementA &&
|
|
846
|
+
typeof elementA.nodeType === 'number'
|
|
847
|
+
? elementA
|
|
848
|
+
: null;
|
|
849
|
+
const domNodeB = typeof elementB === 'object' &&
|
|
850
|
+
'nodeType' in elementB &&
|
|
851
|
+
typeof elementB.nodeType === 'number'
|
|
852
|
+
? elementB
|
|
853
|
+
: null;
|
|
854
|
+
applySkipLogic(objA, domNodeA);
|
|
855
|
+
applySkipLogic(objB, domNodeB);
|
|
856
|
+
// Quick check: if both marked as skipFull, return empty
|
|
857
|
+
if (objA.skipFull && objB.skipFull) {
|
|
858
|
+
return [];
|
|
859
|
+
}
|
|
860
|
+
// Perform diff
|
|
861
|
+
const diffCount = { value: 0 };
|
|
862
|
+
let diffs = diffNode(objA, objB, [], mergedOptions, diffCount);
|
|
863
|
+
// Apply filterOuterDiff hook
|
|
864
|
+
if (mergedOptions.filterOuterDiff) {
|
|
865
|
+
diffs = mergedOptions.filterOuterDiff(objA, objB, diffs) || diffs;
|
|
866
|
+
}
|
|
867
|
+
return diffs;
|
|
868
|
+
};
|
|
869
|
+
const applyDiff = (element, diff, options) => {
|
|
870
|
+
try {
|
|
871
|
+
// Call preDiffApply hook
|
|
872
|
+
if (options.preDiffApply) {
|
|
873
|
+
const skip = options.preDiffApply({ diff, node: element });
|
|
874
|
+
if (skip === true)
|
|
875
|
+
return true;
|
|
876
|
+
}
|
|
877
|
+
const target = getNodeByRoute(element, diff.route);
|
|
878
|
+
if (!target && diff.action !== ACTIONS.ADD_ELEMENT) {
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
881
|
+
switch (diff.action) {
|
|
882
|
+
case ACTIONS.ADD_ELEMENT: {
|
|
883
|
+
const parent = diff.route.length === 0
|
|
884
|
+
? element
|
|
885
|
+
: getNodeByRoute(element, diff.route);
|
|
886
|
+
if (!parent || !diff.element)
|
|
887
|
+
return false;
|
|
888
|
+
const newNode = objToNode(diff.element, options);
|
|
889
|
+
if (!newNode)
|
|
890
|
+
return false;
|
|
891
|
+
if (diff.index !== undefined &&
|
|
892
|
+
diff.index < parent.childNodes.length) {
|
|
893
|
+
parent.insertBefore(newNode, parent.childNodes[diff.index]);
|
|
894
|
+
}
|
|
895
|
+
else {
|
|
896
|
+
parent.appendChild(newNode);
|
|
897
|
+
}
|
|
898
|
+
break;
|
|
899
|
+
}
|
|
900
|
+
case ACTIONS.REMOVE_ELEMENT: {
|
|
901
|
+
if (!target)
|
|
902
|
+
return false;
|
|
903
|
+
if (target.parentNode) {
|
|
904
|
+
target.parentNode.removeChild(target);
|
|
905
|
+
}
|
|
906
|
+
break;
|
|
907
|
+
}
|
|
908
|
+
case ACTIONS.RELOCATE_ELEMENT: {
|
|
909
|
+
if (!diff.from || !diff.to)
|
|
910
|
+
return false;
|
|
911
|
+
const fromNode = getNodeByRoute(element, diff.from);
|
|
912
|
+
const toParent = diff.to.length === 1
|
|
913
|
+
? element
|
|
914
|
+
: getNodeByRoute(element, diff.to.slice(0, -1));
|
|
915
|
+
if (!fromNode || !toParent)
|
|
916
|
+
return false;
|
|
917
|
+
const toIndex = diff.to[diff.to.length - 1];
|
|
918
|
+
if (toIndex < toParent.childNodes.length) {
|
|
919
|
+
toParent.insertBefore(fromNode, toParent.childNodes[toIndex]);
|
|
920
|
+
}
|
|
921
|
+
else {
|
|
922
|
+
toParent.appendChild(fromNode);
|
|
923
|
+
}
|
|
924
|
+
break;
|
|
925
|
+
}
|
|
926
|
+
case ACTIONS.MODIFY_TEXT: {
|
|
927
|
+
if (!target)
|
|
928
|
+
return false;
|
|
929
|
+
if (target.nodeType === NODE_TYPES.TEXT ||
|
|
930
|
+
target.nodeType === NODE_TYPES.COMMENT) {
|
|
931
|
+
const textNode = target;
|
|
932
|
+
if (options.textDiff &&
|
|
933
|
+
typeof diff.oldValue === 'string' &&
|
|
934
|
+
typeof diff.newValue === 'string') {
|
|
935
|
+
options.textDiff(target, textNode.data, diff.oldValue, diff.newValue);
|
|
936
|
+
}
|
|
937
|
+
else if (typeof diff.newValue === 'string') {
|
|
938
|
+
textNode.data = diff.newValue;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
break;
|
|
942
|
+
}
|
|
943
|
+
case ACTIONS.REPLACE_ELEMENT: {
|
|
944
|
+
if (!target || typeof diff.newValue !== 'object')
|
|
945
|
+
return false;
|
|
946
|
+
const newNode = objToNode(diff.newValue, options);
|
|
947
|
+
if (!newNode)
|
|
948
|
+
return false;
|
|
949
|
+
if (target.parentNode) {
|
|
950
|
+
target.parentNode.replaceChild(newNode, target);
|
|
951
|
+
}
|
|
952
|
+
break;
|
|
953
|
+
}
|
|
954
|
+
case ACTIONS.ADD_ATTRIBUTE: {
|
|
955
|
+
if (!target || !diff.name || typeof diff.value !== 'string')
|
|
956
|
+
return false;
|
|
957
|
+
if (target.nodeType === NODE_TYPES.ELEMENT) {
|
|
958
|
+
target.setAttribute(diff.name, diff.value);
|
|
959
|
+
}
|
|
960
|
+
break;
|
|
961
|
+
}
|
|
962
|
+
case ACTIONS.REMOVE_ATTRIBUTE: {
|
|
963
|
+
if (!target || !diff.name)
|
|
964
|
+
return false;
|
|
965
|
+
if (target.nodeType === NODE_TYPES.ELEMENT) {
|
|
966
|
+
target.removeAttribute(diff.name);
|
|
967
|
+
}
|
|
968
|
+
break;
|
|
969
|
+
}
|
|
970
|
+
case ACTIONS.MODIFY_ATTRIBUTE: {
|
|
971
|
+
if (!target || !diff.name || typeof diff.newValue !== 'string')
|
|
972
|
+
return false;
|
|
973
|
+
if (target.nodeType === NODE_TYPES.ELEMENT) {
|
|
974
|
+
target.setAttribute(diff.name, diff.newValue);
|
|
975
|
+
}
|
|
976
|
+
break;
|
|
977
|
+
}
|
|
978
|
+
case ACTIONS.MODIFY_VALUE: {
|
|
979
|
+
if (!target || typeof diff.newValue !== 'string')
|
|
980
|
+
return false;
|
|
981
|
+
target.value = diff.newValue;
|
|
982
|
+
break;
|
|
983
|
+
}
|
|
984
|
+
case ACTIONS.MODIFY_CHECKED: {
|
|
985
|
+
if (!target || typeof diff.newValue !== 'boolean')
|
|
986
|
+
return false;
|
|
987
|
+
target.checked = diff.newValue;
|
|
988
|
+
break;
|
|
989
|
+
}
|
|
990
|
+
case ACTIONS.MODIFY_SELECTED: {
|
|
991
|
+
if (!target || typeof diff.newValue !== 'boolean')
|
|
992
|
+
return false;
|
|
993
|
+
target.selected = diff.newValue;
|
|
994
|
+
break;
|
|
995
|
+
}
|
|
996
|
+
default:
|
|
997
|
+
return false;
|
|
998
|
+
}
|
|
999
|
+
// Call postDiffApply hook
|
|
1000
|
+
if (options.postDiffApply) {
|
|
1001
|
+
options.postDiffApply({ diff, node: element });
|
|
1002
|
+
}
|
|
1003
|
+
return true;
|
|
1004
|
+
}
|
|
1005
|
+
catch (error) {
|
|
1006
|
+
if (options.debug) {
|
|
1007
|
+
console.error('Error applying diff:', error, diff);
|
|
1008
|
+
}
|
|
1009
|
+
return false;
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
export const apply = (element, diffs, options = {}) => {
|
|
1013
|
+
const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
|
|
1014
|
+
if (!Array.isArray(diffs)) {
|
|
1015
|
+
return false;
|
|
1016
|
+
}
|
|
1017
|
+
for (const diff of diffs) {
|
|
1018
|
+
const success = applyDiff(element, diff, mergedOptions);
|
|
1019
|
+
if (!success) {
|
|
1020
|
+
return false;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
return true;
|
|
1024
|
+
};
|
|
1025
|
+
const invertDiff = (diff) => {
|
|
1026
|
+
const inverted = { ...diff };
|
|
1027
|
+
switch (diff.action) {
|
|
1028
|
+
case ACTIONS.ADD_ELEMENT:
|
|
1029
|
+
inverted.action = ACTIONS.REMOVE_ELEMENT;
|
|
1030
|
+
break;
|
|
1031
|
+
case ACTIONS.REMOVE_ELEMENT:
|
|
1032
|
+
inverted.action = ACTIONS.ADD_ELEMENT;
|
|
1033
|
+
inverted.index = diff.route[diff.route.length - 1];
|
|
1034
|
+
break;
|
|
1035
|
+
case ACTIONS.RELOCATE_ELEMENT:
|
|
1036
|
+
inverted.from = diff.to;
|
|
1037
|
+
inverted.to = diff.from;
|
|
1038
|
+
break;
|
|
1039
|
+
case ACTIONS.MODIFY_TEXT:
|
|
1040
|
+
inverted.oldValue = diff.newValue;
|
|
1041
|
+
inverted.newValue = diff.oldValue;
|
|
1042
|
+
break;
|
|
1043
|
+
case ACTIONS.REPLACE_ELEMENT:
|
|
1044
|
+
inverted.oldValue = diff.newValue;
|
|
1045
|
+
inverted.newValue = diff.oldValue;
|
|
1046
|
+
break;
|
|
1047
|
+
case ACTIONS.ADD_ATTRIBUTE:
|
|
1048
|
+
inverted.action = ACTIONS.REMOVE_ATTRIBUTE;
|
|
1049
|
+
break;
|
|
1050
|
+
case ACTIONS.REMOVE_ATTRIBUTE:
|
|
1051
|
+
inverted.action = ACTIONS.ADD_ATTRIBUTE;
|
|
1052
|
+
break;
|
|
1053
|
+
case ACTIONS.MODIFY_ATTRIBUTE:
|
|
1054
|
+
inverted.oldValue = diff.newValue;
|
|
1055
|
+
inverted.newValue = diff.oldValue;
|
|
1056
|
+
break;
|
|
1057
|
+
case ACTIONS.MODIFY_VALUE:
|
|
1058
|
+
case ACTIONS.MODIFY_CHECKED:
|
|
1059
|
+
case ACTIONS.MODIFY_SELECTED:
|
|
1060
|
+
inverted.oldValue = diff.newValue;
|
|
1061
|
+
inverted.newValue = diff.oldValue;
|
|
1062
|
+
break;
|
|
1063
|
+
}
|
|
1064
|
+
return inverted;
|
|
1065
|
+
};
|
|
1066
|
+
export const undo = (element, diffs, options = {}) => {
|
|
1067
|
+
if (!Array.isArray(diffs)) {
|
|
1068
|
+
return false;
|
|
1069
|
+
}
|
|
1070
|
+
const reversedDiffs = [...diffs].reverse();
|
|
1071
|
+
const invertedDiffs = reversedDiffs.map((diff) => invertDiff(diff));
|
|
1072
|
+
return apply(element, invertedDiffs, options);
|
|
1073
|
+
};
|
|
1074
|
+
export const createDiffDOM = (userOptions = {}) => {
|
|
1075
|
+
const options = { ...DEFAULT_OPTIONS, ...userOptions };
|
|
1076
|
+
return {
|
|
1077
|
+
diff: (elementA, elementB) => diff(elementA, elementB, options),
|
|
1078
|
+
apply: (element, diffs) => apply(element, diffs, options),
|
|
1079
|
+
undo: (element, diffs) => undo(element, diffs, options),
|
|
1080
|
+
};
|
|
1081
|
+
};
|