opalla 0.1.0 → 0.1.1
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.
- checksums.yaml +4 -4
- data/README.md +394 -14
- data/lib/opalla.rb +11 -4
- data/lib/opalla/component_helper.rb +28 -0
- data/lib/opalla/controller_add_on.rb +11 -0
- data/lib/opalla/engine.rb +27 -0
- data/lib/opalla/middleware.rb +20 -0
- data/lib/opalla/util.rb +63 -0
- data/lib/opalla/version.rb +1 -1
- data/lib/rails/generators/opalla/assets_generator.rb +23 -0
- data/lib/rails/generators/opalla/collection_generator.rb +19 -0
- data/lib/rails/generators/opalla/component_generator.rb +27 -0
- data/lib/rails/generators/opalla/install_generator.rb +62 -0
- data/lib/rails/generators/opalla/model_generator.rb +19 -0
- data/opal/collection.rb +71 -0
- data/opal/component.rb +136 -0
- data/opal/controller.rb +60 -0
- data/opal/diffDOM.js +1371 -0
- data/opal/diff_dom.rb +26 -0
- data/opal/element.rb +17 -0
- data/opal/hex_random.rb +12 -0
- data/opal/model.rb +50 -0
- data/opal/opalla.rb +21 -0
- data/opal/router.rb +66 -0
- data/opal/sha1.js +366 -0
- data/opal/view_helper.rb +168 -0
- data/opalla.gemspec +8 -0
- data/opalla.gif +0 -0
- metadata +109 -2
data/opal/controller.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
module Opalla
|
2
|
+
class Controller < Component
|
3
|
+
attr_reader :el
|
4
|
+
|
5
|
+
def initialize(action=nil, params=nil, template: nil)
|
6
|
+
@bindings = {}
|
7
|
+
register_exposed_objects
|
8
|
+
super(template: "#{controller_name}/#{action}")
|
9
|
+
@el = Document.body
|
10
|
+
self.send(action)
|
11
|
+
end
|
12
|
+
|
13
|
+
def el
|
14
|
+
@el
|
15
|
+
end
|
16
|
+
|
17
|
+
def render
|
18
|
+
# target = Document.body.clone.html(template.render(self))
|
19
|
+
# el.morph(Element[target])
|
20
|
+
el.html(template.render(self))
|
21
|
+
bind_events
|
22
|
+
end
|
23
|
+
|
24
|
+
def el_selector
|
25
|
+
self.class.instance_variable_get :@el_selector
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
def register_exposed_objects
|
31
|
+
Marshal.load($$.opalla_data)[:vars].each do |key, val|
|
32
|
+
define_singleton_method(key) { val }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.el(selector)
|
37
|
+
@el_selector = selector
|
38
|
+
end
|
39
|
+
|
40
|
+
def bind(binding)
|
41
|
+
@bindings.merge!(binding)
|
42
|
+
binding.each do |key, attrs|
|
43
|
+
model = send(key)
|
44
|
+
id = model.model_id
|
45
|
+
model.bind(*attrs, -> { render })
|
46
|
+
merge_events_hash({
|
47
|
+
%Q(input [data-model-id="#{id}"] [data-bind]) => -> target do
|
48
|
+
model.find(target)
|
49
|
+
attr = target.data('bind')
|
50
|
+
model.public_send(:"#{attr}=", target.value)
|
51
|
+
end
|
52
|
+
})
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def controller_name
|
57
|
+
self.class.to_s.underscore.gsub('_controller', '')
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/opal/diffDOM.js
ADDED
@@ -0,0 +1,1371 @@
|
|
1
|
+
(function() {
|
2
|
+
"use strict";
|
3
|
+
|
4
|
+
var diffcount;
|
5
|
+
|
6
|
+
var Diff = function(options) {
|
7
|
+
var diff = this;
|
8
|
+
if (options) {
|
9
|
+
Object.keys(options).forEach(function(option) {
|
10
|
+
diff[option] = options[option];
|
11
|
+
});
|
12
|
+
}
|
13
|
+
|
14
|
+
};
|
15
|
+
|
16
|
+
Diff.prototype = {
|
17
|
+
toString: function() {
|
18
|
+
return JSON.stringify(this);
|
19
|
+
},
|
20
|
+
setValue: function(aKey, aValue) {
|
21
|
+
this[aKey] = aValue;
|
22
|
+
return this;
|
23
|
+
}
|
24
|
+
};
|
25
|
+
|
26
|
+
var SubsetMapping = function SubsetMapping(a, b) {
|
27
|
+
this.oldValue = a;
|
28
|
+
this.newValue = b;
|
29
|
+
};
|
30
|
+
|
31
|
+
SubsetMapping.prototype = {
|
32
|
+
contains: function contains(subset) {
|
33
|
+
if (subset.length < this.length) {
|
34
|
+
return subset.newValue >= this.newValue && subset.newValue < this.newValue + this.length;
|
35
|
+
}
|
36
|
+
return false;
|
37
|
+
},
|
38
|
+
toString: function toString() {
|
39
|
+
return this.length + " element subset, first mapping: old " + this.oldValue + " → new " + this.newValue;
|
40
|
+
}
|
41
|
+
};
|
42
|
+
|
43
|
+
var elementDescriptors = function(el) {
|
44
|
+
var output = [];
|
45
|
+
if (el.nodeName !== '#text' && el.nodeName !== '#comment') {
|
46
|
+
output.push(el.nodeName);
|
47
|
+
if (el.attributes) {
|
48
|
+
if (el.attributes['class']) {
|
49
|
+
output.push(el.nodeName + '.' + el.attributes['class'].replace(/ /g, '.'));
|
50
|
+
}
|
51
|
+
if (el.attributes.id) {
|
52
|
+
output.push(el.nodeName + '#' + el.attributes.id);
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
}
|
57
|
+
return output;
|
58
|
+
};
|
59
|
+
|
60
|
+
var findUniqueDescriptors = function(li) {
|
61
|
+
var uniqueDescriptors = {},
|
62
|
+
duplicateDescriptors = {};
|
63
|
+
|
64
|
+
li.forEach(function(node) {
|
65
|
+
elementDescriptors(node).forEach(function(descriptor) {
|
66
|
+
var inUnique = descriptor in uniqueDescriptors,
|
67
|
+
inDupes = descriptor in duplicateDescriptors;
|
68
|
+
if (!inUnique && !inDupes) {
|
69
|
+
uniqueDescriptors[descriptor] = true;
|
70
|
+
} else if (inUnique) {
|
71
|
+
delete uniqueDescriptors[descriptor];
|
72
|
+
duplicateDescriptors[descriptor] = true;
|
73
|
+
}
|
74
|
+
});
|
75
|
+
|
76
|
+
});
|
77
|
+
|
78
|
+
return uniqueDescriptors;
|
79
|
+
};
|
80
|
+
|
81
|
+
var uniqueInBoth = function(l1, l2) {
|
82
|
+
var l1Unique = findUniqueDescriptors(l1),
|
83
|
+
l2Unique = findUniqueDescriptors(l2),
|
84
|
+
inBoth = {};
|
85
|
+
|
86
|
+
Object.keys(l1Unique).forEach(function(key) {
|
87
|
+
if (l2Unique[key]) {
|
88
|
+
inBoth[key] = true;
|
89
|
+
}
|
90
|
+
});
|
91
|
+
|
92
|
+
return inBoth;
|
93
|
+
};
|
94
|
+
|
95
|
+
var removeDone = function(tree) {
|
96
|
+
delete tree.outerDone;
|
97
|
+
delete tree.innerDone;
|
98
|
+
delete tree.valueDone;
|
99
|
+
if (tree.childNodes) {
|
100
|
+
return tree.childNodes.every(removeDone);
|
101
|
+
} else {
|
102
|
+
return true;
|
103
|
+
}
|
104
|
+
};
|
105
|
+
|
106
|
+
var isEqual = function(e1, e2) {
|
107
|
+
|
108
|
+
var e1Attributes, e2Attributes;
|
109
|
+
|
110
|
+
if (!['nodeName', 'value', 'checked', 'selected', 'data'].every(function(element) {
|
111
|
+
if (e1[element] !== e2[element]) {
|
112
|
+
return false;
|
113
|
+
}
|
114
|
+
return true;
|
115
|
+
})) {
|
116
|
+
return false;
|
117
|
+
}
|
118
|
+
|
119
|
+
if (Boolean(e1.attributes) !== Boolean(e2.attributes)) {
|
120
|
+
return false;
|
121
|
+
}
|
122
|
+
|
123
|
+
if (Boolean(e1.childNodes) !== Boolean(e2.childNodes)) {
|
124
|
+
return false;
|
125
|
+
}
|
126
|
+
|
127
|
+
if (e1.attributes) {
|
128
|
+
e1Attributes = Object.keys(e1.attributes);
|
129
|
+
e2Attributes = Object.keys(e2.attributes);
|
130
|
+
|
131
|
+
if (e1Attributes.length !== e2Attributes.length) {
|
132
|
+
return false;
|
133
|
+
}
|
134
|
+
if (!e1Attributes.every(function(attribute) {
|
135
|
+
if (e1.attributes[attribute] !== e2.attributes[attribute]) {
|
136
|
+
return false;
|
137
|
+
}
|
138
|
+
})) {
|
139
|
+
return false;
|
140
|
+
}
|
141
|
+
}
|
142
|
+
|
143
|
+
if (e1.childNodes) {
|
144
|
+
if (e1.childNodes.length !== e2.childNodes.length) {
|
145
|
+
return false;
|
146
|
+
}
|
147
|
+
if (!e1.childNodes.every(function(childNode, index) {
|
148
|
+
return isEqual(childNode, e2.childNodes[index]);
|
149
|
+
})) {
|
150
|
+
|
151
|
+
return false;
|
152
|
+
}
|
153
|
+
|
154
|
+
}
|
155
|
+
|
156
|
+
return true;
|
157
|
+
|
158
|
+
};
|
159
|
+
|
160
|
+
|
161
|
+
var roughlyEqual = function(e1, e2, uniqueDescriptors, sameSiblings, preventRecursion) {
|
162
|
+
var childUniqueDescriptors, nodeList1, nodeList2;
|
163
|
+
|
164
|
+
if (!e1 || !e2) {
|
165
|
+
return false;
|
166
|
+
}
|
167
|
+
|
168
|
+
if (e1.nodeName !== e2.nodeName) {
|
169
|
+
return false;
|
170
|
+
}
|
171
|
+
|
172
|
+
if (e1.nodeName === '#text') {
|
173
|
+
// Note that we initially don't care what the text content of a node is,
|
174
|
+
// the mere fact that it's the same tag and "has text" means it's roughly
|
175
|
+
// equal, and then we can find out the true text difference later.
|
176
|
+
return preventRecursion ? true : e1.data === e2.data;
|
177
|
+
}
|
178
|
+
|
179
|
+
|
180
|
+
if (e1.nodeName in uniqueDescriptors) {
|
181
|
+
return true;
|
182
|
+
}
|
183
|
+
|
184
|
+
if (e1.attributes && e2.attributes) {
|
185
|
+
|
186
|
+
if (e1.attributes.id && e1.attributes.id === e2.attributes.id) {
|
187
|
+
var idDescriptor = e1.nodeName + '#' + e1.attributes.id;
|
188
|
+
if (idDescriptor in uniqueDescriptors) {
|
189
|
+
return true;
|
190
|
+
}
|
191
|
+
}
|
192
|
+
if (e1.attributes['class'] && e1.attributes['class'] === e2.attributes['class']) {
|
193
|
+
var classDescriptor = e1.nodeName + '.' + e1.attributes['class'].replace(/ /g, '.');
|
194
|
+
if (classDescriptor in uniqueDescriptors) {
|
195
|
+
return true;
|
196
|
+
}
|
197
|
+
}
|
198
|
+
}
|
199
|
+
|
200
|
+
if (sameSiblings) {
|
201
|
+
return true;
|
202
|
+
}
|
203
|
+
|
204
|
+
nodeList1 = e1.childNodes ? e1.childNodes.slice().reverse() : [];
|
205
|
+
nodeList2 = e2.childNodes ? e2.childNodes.slice().reverse() : [];
|
206
|
+
|
207
|
+
if (nodeList1.length !== nodeList2.length) {
|
208
|
+
return false;
|
209
|
+
}
|
210
|
+
|
211
|
+
if (preventRecursion) {
|
212
|
+
return nodeList1.every(function(element, index) {
|
213
|
+
return element.nodeName === nodeList2[index].nodeName;
|
214
|
+
});
|
215
|
+
} else {
|
216
|
+
// note: we only allow one level of recursion at any depth. If 'preventRecursion'
|
217
|
+
// was not set, we must explicitly force it to true for child iterations.
|
218
|
+
childUniqueDescriptors = uniqueInBoth(nodeList1, nodeList2);
|
219
|
+
return nodeList1.every(function(element, index) {
|
220
|
+
return roughlyEqual(element, nodeList2[index], childUniqueDescriptors, true, true);
|
221
|
+
});
|
222
|
+
}
|
223
|
+
};
|
224
|
+
|
225
|
+
|
226
|
+
var cloneObj = function(obj) {
|
227
|
+
// TODO: Do we really need to clone here? Is it not enough to just return the original object?
|
228
|
+
return JSON.parse(JSON.stringify(obj));
|
229
|
+
//return obj;
|
230
|
+
};
|
231
|
+
|
232
|
+
/**
|
233
|
+
* based on https://en.wikibooks.org/wiki/Algorithm_implementation/Strings/Longest_common_substring#JavaScript
|
234
|
+
*/
|
235
|
+
var findCommonSubsets = function(c1, c2, marked1, marked2) {
|
236
|
+
var lcsSize = 0,
|
237
|
+
index = [],
|
238
|
+
matches = Array.apply(null, new Array(c1.length + 1)).map(function() {
|
239
|
+
return [];
|
240
|
+
}), // set up the matching table
|
241
|
+
uniqueDescriptors = uniqueInBoth(c1, c2),
|
242
|
+
// If all of the elements are the same tag, id and class, then we can
|
243
|
+
// consider them roughly the same even if they have a different number of
|
244
|
+
// children. This will reduce removing and re-adding similar elements.
|
245
|
+
subsetsSame = c1.length === c2.length,
|
246
|
+
origin, ret;
|
247
|
+
|
248
|
+
if (subsetsSame) {
|
249
|
+
|
250
|
+
c1.some(function(element, i) {
|
251
|
+
var c1Desc = elementDescriptors(element),
|
252
|
+
c2Desc = elementDescriptors(c2[i]);
|
253
|
+
if (c1Desc.length !== c2Desc.length) {
|
254
|
+
subsetsSame = false;
|
255
|
+
return true;
|
256
|
+
}
|
257
|
+
c1Desc.some(function(description, i) {
|
258
|
+
if (description !== c2Desc[i]) {
|
259
|
+
subsetsSame = false;
|
260
|
+
return true;
|
261
|
+
}
|
262
|
+
});
|
263
|
+
if (!subsetsSame) {
|
264
|
+
return true;
|
265
|
+
}
|
266
|
+
|
267
|
+
});
|
268
|
+
}
|
269
|
+
|
270
|
+
// fill the matches with distance values
|
271
|
+
c1.forEach(function(c1Element, c1Index) {
|
272
|
+
c2.forEach(function(c2Element, c2Index) {
|
273
|
+
if (!marked1[c1Index] && !marked2[c2Index] && roughlyEqual(c1Element, c2Element, uniqueDescriptors, subsetsSame)) {
|
274
|
+
matches[c1Index + 1][c2Index + 1] = (matches[c1Index][c2Index] ? matches[c1Index][c2Index] + 1 : 1);
|
275
|
+
if (matches[c1Index + 1][c2Index + 1] >= lcsSize) {
|
276
|
+
lcsSize = matches[c1Index + 1][c2Index + 1];
|
277
|
+
index = [c1Index + 1, c2Index + 1];
|
278
|
+
}
|
279
|
+
} else {
|
280
|
+
matches[c1Index + 1][c2Index + 1] = 0;
|
281
|
+
}
|
282
|
+
});
|
283
|
+
});
|
284
|
+
if (lcsSize === 0) {
|
285
|
+
return false;
|
286
|
+
}
|
287
|
+
origin = [index[0] - lcsSize, index[1] - lcsSize];
|
288
|
+
ret = new SubsetMapping(origin[0], origin[1]);
|
289
|
+
ret.length = lcsSize;
|
290
|
+
|
291
|
+
return ret;
|
292
|
+
};
|
293
|
+
|
294
|
+
/**
|
295
|
+
* This should really be a predefined function in Array...
|
296
|
+
*/
|
297
|
+
var makeArray = function(n, v) {
|
298
|
+
return Array.apply(null, new Array(n)).map(function() {
|
299
|
+
return v;
|
300
|
+
});
|
301
|
+
};
|
302
|
+
|
303
|
+
/**
|
304
|
+
* Generate arrays that indicate which node belongs to which subset,
|
305
|
+
* or whether it's actually an orphan node, existing in only one
|
306
|
+
* of the two trees, rather than somewhere in both.
|
307
|
+
*
|
308
|
+
* So if t1 = <img><canvas><br>, t2 = <canvas><br><img>.
|
309
|
+
* The longest subset is "<canvas><br>" (length 2), so it will group 0.
|
310
|
+
* The second longest is "<img>" (length 1), so it will be group 1.
|
311
|
+
* gaps1 will therefore be [1,0,0] and gaps2 [0,0,1].
|
312
|
+
*
|
313
|
+
* If an element is not part of any group, it will stay being 'true', which
|
314
|
+
* is the initial value. For example:
|
315
|
+
* t1 = <img><p></p><br><canvas>, t2 = <b></b><br><canvas><img>
|
316
|
+
*
|
317
|
+
* The "<p></p>" and "<b></b>" do only show up in one of the two and will
|
318
|
+
* therefore be marked by "true". The remaining parts are parts of the
|
319
|
+
* groups 0 and 1:
|
320
|
+
* gaps1 = [1, true, 0, 0], gaps2 = [true, 0, 0, 1]
|
321
|
+
*
|
322
|
+
*/
|
323
|
+
var getGapInformation = function(t1, t2, stable) {
|
324
|
+
|
325
|
+
var gaps1 = t1.childNodes ? makeArray(t1.childNodes.length, true) : [],
|
326
|
+
gaps2 = t2.childNodes ? makeArray(t2.childNodes.length, true) : [],
|
327
|
+
group = 0;
|
328
|
+
|
329
|
+
// give elements from the same subset the same group number
|
330
|
+
stable.forEach(function(subset) {
|
331
|
+
var i, endOld = subset.oldValue + subset.length,
|
332
|
+
endNew = subset.newValue + subset.length;
|
333
|
+
for (i = subset.oldValue; i < endOld; i += 1) {
|
334
|
+
gaps1[i] = group;
|
335
|
+
}
|
336
|
+
for (i = subset.newValue; i < endNew; i += 1) {
|
337
|
+
gaps2[i] = group;
|
338
|
+
}
|
339
|
+
group += 1;
|
340
|
+
});
|
341
|
+
|
342
|
+
return {
|
343
|
+
gaps1: gaps1,
|
344
|
+
gaps2: gaps2
|
345
|
+
};
|
346
|
+
};
|
347
|
+
|
348
|
+
/**
|
349
|
+
* Find all matching subsets, based on immediate child differences only.
|
350
|
+
*/
|
351
|
+
var markSubTrees = function(oldTree, newTree) {
|
352
|
+
// note: the child lists are views, and so update as we update old/newTree
|
353
|
+
var oldChildren = oldTree.childNodes ? oldTree.childNodes : [],
|
354
|
+
newChildren = newTree.childNodes ? newTree.childNodes : [],
|
355
|
+
marked1 = makeArray(oldChildren.length, false),
|
356
|
+
marked2 = makeArray(newChildren.length, false),
|
357
|
+
subsets = [],
|
358
|
+
subset = true,
|
359
|
+
returnIndex = function() {
|
360
|
+
return arguments[1];
|
361
|
+
},
|
362
|
+
markBoth = function(i) {
|
363
|
+
marked1[subset.oldValue + i] = true;
|
364
|
+
marked2[subset.newValue + i] = true;
|
365
|
+
};
|
366
|
+
|
367
|
+
while (subset) {
|
368
|
+
subset = findCommonSubsets(oldChildren, newChildren, marked1, marked2);
|
369
|
+
if (subset) {
|
370
|
+
subsets.push(subset);
|
371
|
+
|
372
|
+
Array.apply(null, new Array(subset.length)).map(returnIndex).forEach(markBoth);
|
373
|
+
|
374
|
+
}
|
375
|
+
}
|
376
|
+
return subsets;
|
377
|
+
};
|
378
|
+
|
379
|
+
|
380
|
+
function swap(obj, p1, p2) {
|
381
|
+
(function(_) {
|
382
|
+
obj[p1] = obj[p2];
|
383
|
+
obj[p2] = _;
|
384
|
+
}(obj[p1]));
|
385
|
+
}
|
386
|
+
|
387
|
+
|
388
|
+
var DiffTracker = function() {
|
389
|
+
this.list = [];
|
390
|
+
};
|
391
|
+
|
392
|
+
DiffTracker.prototype = {
|
393
|
+
list: false,
|
394
|
+
add: function(diffs) {
|
395
|
+
var list = this.list;
|
396
|
+
diffs.forEach(function(diff) {
|
397
|
+
list.push(diff);
|
398
|
+
});
|
399
|
+
},
|
400
|
+
forEach: function(fn) {
|
401
|
+
this.list.forEach(fn);
|
402
|
+
}
|
403
|
+
};
|
404
|
+
|
405
|
+
var diffDOM = function(options) {
|
406
|
+
|
407
|
+
var defaults = {
|
408
|
+
debug: false,
|
409
|
+
diffcap: 10, // Limit for how many diffs are accepting when debugging. Inactive when debug is false.
|
410
|
+
maxDepth: false, // False or a numeral. If set to a numeral, limits the level of depth that the the diff mechanism looks for differences. If false, goes through the entire tree.
|
411
|
+
valueDiffing: true, // Whether to take into consideration the values of forms that differ from auto assigned values (when a user fills out a form).
|
412
|
+
// syntax: textDiff: function (node, currentValue, expectedValue, newValue)
|
413
|
+
textDiff: function() {
|
414
|
+
arguments[0].data = arguments[3];
|
415
|
+
return;
|
416
|
+
},
|
417
|
+
// empty functions were benchmarked as running faster than both
|
418
|
+
// `f && f()` and `if (f) { f(); }`
|
419
|
+
preVirtualDiffApply: function() {},
|
420
|
+
postVirtualDiffApply: function() {},
|
421
|
+
preDiffApply: function() {},
|
422
|
+
postDiffApply: function() {},
|
423
|
+
filterOuterDiff: null
|
424
|
+
},
|
425
|
+
i;
|
426
|
+
|
427
|
+
if (typeof options === "undefined") {
|
428
|
+
options = {};
|
429
|
+
}
|
430
|
+
|
431
|
+
for (i in defaults) {
|
432
|
+
if (typeof options[i] === "undefined") {
|
433
|
+
this[i] = defaults[i];
|
434
|
+
} else {
|
435
|
+
this[i] = options[i];
|
436
|
+
}
|
437
|
+
}
|
438
|
+
|
439
|
+
this._const = {
|
440
|
+
addAttribute: 0,
|
441
|
+
modifyAttribute: 1,
|
442
|
+
removeAttribute: 2,
|
443
|
+
modifyTextElement: 3,
|
444
|
+
relocateGroup: 4,
|
445
|
+
removeElement: 5,
|
446
|
+
addElement: 6,
|
447
|
+
removeTextElement: 7,
|
448
|
+
addTextElement: 8,
|
449
|
+
replaceElement: 9,
|
450
|
+
modifyValue: 10,
|
451
|
+
modifyChecked: 11,
|
452
|
+
modifySelected: 12,
|
453
|
+
modifyComment: 13,
|
454
|
+
action: 'a',
|
455
|
+
route: 'r',
|
456
|
+
oldValue: 'o',
|
457
|
+
newValue: 'n',
|
458
|
+
element: 'e',
|
459
|
+
'group': 'g',
|
460
|
+
from: 'f',
|
461
|
+
to: 't',
|
462
|
+
name: 'na',
|
463
|
+
value: 'v',
|
464
|
+
'data': 'd',
|
465
|
+
'attributes': 'at',
|
466
|
+
'nodeName': 'nn',
|
467
|
+
'childNodes': 'c',
|
468
|
+
'checked': 'ch',
|
469
|
+
'selected': 's'
|
470
|
+
};
|
471
|
+
};
|
472
|
+
|
473
|
+
diffDOM.Diff = Diff;
|
474
|
+
|
475
|
+
diffDOM.prototype = {
|
476
|
+
|
477
|
+
// ===== Create a diff =====
|
478
|
+
|
479
|
+
diff: function(t1Node, t2Node) {
|
480
|
+
|
481
|
+
var t1 = this.nodeToObj(t1Node),
|
482
|
+
t2 = this.nodeToObj(t2Node);
|
483
|
+
|
484
|
+
diffcount = 0;
|
485
|
+
|
486
|
+
if (this.debug) {
|
487
|
+
this.t1Orig = this.nodeToObj(t1Node);
|
488
|
+
this.t2Orig = this.nodeToObj(t2Node);
|
489
|
+
}
|
490
|
+
|
491
|
+
this.tracker = new DiffTracker();
|
492
|
+
return this.findDiffs(t1, t2);
|
493
|
+
},
|
494
|
+
findDiffs: function(t1, t2) {
|
495
|
+
var diffs;
|
496
|
+
do {
|
497
|
+
if (this.debug) {
|
498
|
+
diffcount += 1;
|
499
|
+
if (diffcount > this.diffcap) {
|
500
|
+
window.diffError = [this.t1Orig, this.t2Orig];
|
501
|
+
throw new Error("surpassed diffcap:" + JSON.stringify(this.t1Orig) + " -> " + JSON.stringify(this.t2Orig));
|
502
|
+
}
|
503
|
+
}
|
504
|
+
diffs = this.findNextDiff(t1, t2, []);
|
505
|
+
if (diffs.length === 0) {
|
506
|
+
// Last check if the elements really are the same now.
|
507
|
+
// If not, remove all info about being done and start over.
|
508
|
+
// Somtimes a node can be marked as done, but the creation of subsequent diffs means that it has to be changed anyway.
|
509
|
+
if (!isEqual(t1, t2)) {
|
510
|
+
removeDone(t1);
|
511
|
+
diffs = this.findNextDiff(t1, t2, []);
|
512
|
+
}
|
513
|
+
}
|
514
|
+
|
515
|
+
if (diffs.length > 0) {
|
516
|
+
this.tracker.add(diffs);
|
517
|
+
this.applyVirtual(t1, diffs);
|
518
|
+
}
|
519
|
+
} while (diffs.length > 0);
|
520
|
+
return this.tracker.list;
|
521
|
+
},
|
522
|
+
findNextDiff: function(t1, t2, route) {
|
523
|
+
var diffs, fdiffs;
|
524
|
+
|
525
|
+
if (this.maxDepth && route.length > this.maxDepth) {
|
526
|
+
return [];
|
527
|
+
}
|
528
|
+
// outer differences?
|
529
|
+
if (!t1.outerDone) {
|
530
|
+
diffs = this.findOuterDiff(t1, t2, route);
|
531
|
+
if (this.filterOuterDiff) {
|
532
|
+
fdiffs = this.filterOuterDiff(t1, t2, diffs);
|
533
|
+
if (fdiffs) diffs = fdiffs;
|
534
|
+
}
|
535
|
+
if (diffs.length > 0) {
|
536
|
+
t1.outerDone = true;
|
537
|
+
return diffs;
|
538
|
+
} else {
|
539
|
+
t1.outerDone = true;
|
540
|
+
}
|
541
|
+
}
|
542
|
+
// inner differences?
|
543
|
+
if (!t1.innerDone) {
|
544
|
+
diffs = this.findInnerDiff(t1, t2, route);
|
545
|
+
if (diffs.length > 0) {
|
546
|
+
return diffs;
|
547
|
+
} else {
|
548
|
+
t1.innerDone = true;
|
549
|
+
}
|
550
|
+
}
|
551
|
+
|
552
|
+
if (this.valueDiffing && !t1.valueDone) {
|
553
|
+
// value differences?
|
554
|
+
diffs = this.findValueDiff(t1, t2, route);
|
555
|
+
|
556
|
+
if (diffs.length > 0) {
|
557
|
+
t1.valueDone = true;
|
558
|
+
return diffs;
|
559
|
+
} else {
|
560
|
+
t1.valueDone = true;
|
561
|
+
}
|
562
|
+
}
|
563
|
+
|
564
|
+
// no differences
|
565
|
+
return [];
|
566
|
+
},
|
567
|
+
findOuterDiff: function(t1, t2, route) {
|
568
|
+
var t = this;
|
569
|
+
var diffs = [],
|
570
|
+
attr1, attr2;
|
571
|
+
|
572
|
+
if (t1.nodeName !== t2.nodeName) {
|
573
|
+
return [new Diff()
|
574
|
+
.setValue(t._const.action, t._const.replaceElement)
|
575
|
+
.setValue(t._const.oldValue, cloneObj(t1))
|
576
|
+
.setValue(t._const.newValue, cloneObj(t2))
|
577
|
+
.setValue(t._const.route, route)
|
578
|
+
];
|
579
|
+
}
|
580
|
+
|
581
|
+
if (t1.data !== t2.data) {
|
582
|
+
// Comment or text node.
|
583
|
+
if (t1.nodeName === '#text') {
|
584
|
+
return [new Diff()
|
585
|
+
.setValue(t._const.action, t._const.modifyTextElement)
|
586
|
+
.setValue(t._const.route, route)
|
587
|
+
.setValue(t._const.oldValue, t1.data)
|
588
|
+
.setValue(t._const.newValue, t2.data)
|
589
|
+
];
|
590
|
+
} else {
|
591
|
+
return [new Diff()
|
592
|
+
.setValue(t._const.action, t._const.modifyComment)
|
593
|
+
.setValue(t._const.route, route)
|
594
|
+
.setValue(t._const.oldValue, t1.data)
|
595
|
+
.setValue(t._const.newValue, t2.data)
|
596
|
+
];
|
597
|
+
}
|
598
|
+
|
599
|
+
}
|
600
|
+
|
601
|
+
|
602
|
+
attr1 = t1.attributes ? Object.keys(t1.attributes).sort() : [];
|
603
|
+
attr2 = t2.attributes ? Object.keys(t2.attributes).sort() : [];
|
604
|
+
|
605
|
+
attr1.forEach(function(attr) {
|
606
|
+
var pos = attr2.indexOf(attr);
|
607
|
+
if (pos === -1) {
|
608
|
+
diffs.push(new Diff()
|
609
|
+
.setValue(t._const.action, t._const.removeAttribute)
|
610
|
+
.setValue(t._const.route, route)
|
611
|
+
.setValue(t._const.name, attr)
|
612
|
+
.setValue(t._const.value, t1.attributes[attr])
|
613
|
+
);
|
614
|
+
} else {
|
615
|
+
attr2.splice(pos, 1);
|
616
|
+
if (t1.attributes[attr] !== t2.attributes[attr]) {
|
617
|
+
diffs.push(new Diff()
|
618
|
+
.setValue(t._const.action, t._const.modifyAttribute)
|
619
|
+
.setValue(t._const.route, route)
|
620
|
+
.setValue(t._const.name, attr)
|
621
|
+
.setValue(t._const.oldValue, t1.attributes[attr])
|
622
|
+
.setValue(t._const.newValue, t2.attributes[attr])
|
623
|
+
);
|
624
|
+
}
|
625
|
+
}
|
626
|
+
|
627
|
+
});
|
628
|
+
|
629
|
+
|
630
|
+
attr2.forEach(function(attr) {
|
631
|
+
diffs.push(new Diff()
|
632
|
+
.setValue(t._const.action, t._const.addAttribute)
|
633
|
+
.setValue(t._const.route, route)
|
634
|
+
.setValue(t._const.name, attr)
|
635
|
+
.setValue(t._const.value, t2.attributes[attr])
|
636
|
+
);
|
637
|
+
|
638
|
+
});
|
639
|
+
|
640
|
+
return diffs;
|
641
|
+
},
|
642
|
+
nodeToObj: function(aNode) {
|
643
|
+
var objNode = {},
|
644
|
+
dobj = this;
|
645
|
+
objNode.nodeName = aNode.nodeName;
|
646
|
+
if (objNode.nodeName === '#text' || objNode.nodeName === '#comment') {
|
647
|
+
objNode.data = aNode.data;
|
648
|
+
} else {
|
649
|
+
if (aNode.attributes && aNode.attributes.length > 0) {
|
650
|
+
objNode.attributes = {};
|
651
|
+
Array.prototype.slice.call(aNode.attributes).forEach(
|
652
|
+
function(attribute) {
|
653
|
+
objNode.attributes[attribute.name] = attribute.value;
|
654
|
+
}
|
655
|
+
);
|
656
|
+
}
|
657
|
+
if (aNode.childNodes && aNode.childNodes.length > 0) {
|
658
|
+
objNode.childNodes = [];
|
659
|
+
Array.prototype.slice.call(aNode.childNodes).forEach(
|
660
|
+
function(childNode) {
|
661
|
+
objNode.childNodes.push(dobj.nodeToObj(childNode));
|
662
|
+
}
|
663
|
+
);
|
664
|
+
}
|
665
|
+
if (this.valueDiffing) {
|
666
|
+
if (aNode.value !== undefined) {
|
667
|
+
objNode.value = aNode.value;
|
668
|
+
}
|
669
|
+
if (aNode.checked !== undefined) {
|
670
|
+
objNode.checked = aNode.checked;
|
671
|
+
}
|
672
|
+
if (aNode.selected !== undefined) {
|
673
|
+
objNode.selected = aNode.selected;
|
674
|
+
}
|
675
|
+
}
|
676
|
+
}
|
677
|
+
|
678
|
+
return objNode;
|
679
|
+
},
|
680
|
+
objToNode: function(objNode, insideSvg) {
|
681
|
+
var node, dobj = this;
|
682
|
+
if (objNode.nodeName === '#text') {
|
683
|
+
node = document.createTextNode(objNode.data);
|
684
|
+
|
685
|
+
} else if (objNode.nodeName === '#comment') {
|
686
|
+
node = document.createComment(objNode.data);
|
687
|
+
} else {
|
688
|
+
if (objNode.nodeName === 'svg' || insideSvg) {
|
689
|
+
node = document.createElementNS('http://www.w3.org/2000/svg', objNode.nodeName);
|
690
|
+
insideSvg = true;
|
691
|
+
} else {
|
692
|
+
node = document.createElement(objNode.nodeName);
|
693
|
+
}
|
694
|
+
if (objNode.attributes) {
|
695
|
+
Object.keys(objNode.attributes).forEach(function(attribute) {
|
696
|
+
node.setAttribute(attribute, objNode.attributes[attribute]);
|
697
|
+
});
|
698
|
+
}
|
699
|
+
if (objNode.childNodes) {
|
700
|
+
objNode.childNodes.forEach(function(childNode) {
|
701
|
+
node.appendChild(dobj.objToNode(childNode, insideSvg));
|
702
|
+
});
|
703
|
+
}
|
704
|
+
if (this.valueDiffing) {
|
705
|
+
if (objNode.value) {
|
706
|
+
node.value = objNode.value;
|
707
|
+
}
|
708
|
+
if (objNode.checked) {
|
709
|
+
node.checked = objNode.checked;
|
710
|
+
}
|
711
|
+
if (objNode.selected) {
|
712
|
+
node.selected = objNode.selected;
|
713
|
+
}
|
714
|
+
}
|
715
|
+
}
|
716
|
+
return node;
|
717
|
+
},
|
718
|
+
findInnerDiff: function(t1, t2, route) {
|
719
|
+
var t = this;
|
720
|
+
var subtrees = (t1.childNodes && t2.childNodes) ? markSubTrees(t1, t2) : [],
|
721
|
+
t1ChildNodes = t1.childNodes ? t1.childNodes : [],
|
722
|
+
t2ChildNodes = t2.childNodes ? t2.childNodes : [],
|
723
|
+
childNodesLengthDifference, diffs = [],
|
724
|
+
index = 0,
|
725
|
+
last, e1, e2, i;
|
726
|
+
|
727
|
+
if (subtrees.length > 0) {
|
728
|
+
/* One or more groups have been identified among the childnodes of t1
|
729
|
+
* and t2.
|
730
|
+
*/
|
731
|
+
diffs = this.attemptGroupRelocation(t1, t2, subtrees, route);
|
732
|
+
if (diffs.length > 0) {
|
733
|
+
return diffs;
|
734
|
+
}
|
735
|
+
}
|
736
|
+
|
737
|
+
/* 0 or 1 groups of similar child nodes have been found
|
738
|
+
* for t1 and t2. 1 If there is 1, it could be a sign that the
|
739
|
+
* contents are the same. When the number of groups is below 2,
|
740
|
+
* t1 and t2 are made to have the same length and each of the
|
741
|
+
* pairs of child nodes are diffed.
|
742
|
+
*/
|
743
|
+
|
744
|
+
|
745
|
+
last = Math.max(t1ChildNodes.length, t2ChildNodes.length);
|
746
|
+
if (t1ChildNodes.length !== t2ChildNodes.length) {
|
747
|
+
childNodesLengthDifference = true;
|
748
|
+
}
|
749
|
+
|
750
|
+
for (i = 0; i < last; i += 1) {
|
751
|
+
e1 = t1ChildNodes[i];
|
752
|
+
e2 = t2ChildNodes[i];
|
753
|
+
|
754
|
+
if (childNodesLengthDifference) {
|
755
|
+
/* t1 and t2 have different amounts of childNodes. Add
|
756
|
+
* and remove as necessary to obtain the same length */
|
757
|
+
if (e1 && !e2) {
|
758
|
+
if (e1.nodeName === '#text') {
|
759
|
+
diffs.push(new Diff()
|
760
|
+
.setValue(t._const.action, t._const.removeTextElement)
|
761
|
+
.setValue(t._const.route, route.concat(index))
|
762
|
+
.setValue(t._const.value, e1.data)
|
763
|
+
);
|
764
|
+
index -= 1;
|
765
|
+
} else {
|
766
|
+
diffs.push(new Diff()
|
767
|
+
.setValue(t._const.action, t._const.removeElement)
|
768
|
+
.setValue(t._const.route, route.concat(index))
|
769
|
+
.setValue(t._const.element, cloneObj(e1))
|
770
|
+
);
|
771
|
+
index -= 1;
|
772
|
+
}
|
773
|
+
|
774
|
+
} else if (e2 && !e1) {
|
775
|
+
if (e2.nodeName === '#text') {
|
776
|
+
diffs.push(new Diff()
|
777
|
+
.setValue(t._const.action, t._const.addTextElement)
|
778
|
+
.setValue(t._const.route, route.concat(index))
|
779
|
+
.setValue(t._const.value, e2.data)
|
780
|
+
);
|
781
|
+
} else {
|
782
|
+
diffs.push(new Diff()
|
783
|
+
.setValue(t._const.action, t._const.addElement)
|
784
|
+
.setValue(t._const.route, route.concat(index))
|
785
|
+
.setValue(t._const.element, cloneObj(e2))
|
786
|
+
);
|
787
|
+
}
|
788
|
+
}
|
789
|
+
}
|
790
|
+
/* We are now guaranteed that childNodes e1 and e2 exist,
|
791
|
+
* and that they can be diffed.
|
792
|
+
*/
|
793
|
+
/* Diffs in child nodes should not affect the parent node,
|
794
|
+
* so we let these diffs be submitted together with other
|
795
|
+
* diffs.
|
796
|
+
*/
|
797
|
+
|
798
|
+
if (e1 && e2) {
|
799
|
+
diffs = diffs.concat(this.findNextDiff(e1, e2, route.concat(index)));
|
800
|
+
}
|
801
|
+
|
802
|
+
index += 1;
|
803
|
+
|
804
|
+
}
|
805
|
+
t1.innerDone = true;
|
806
|
+
return diffs;
|
807
|
+
|
808
|
+
},
|
809
|
+
|
810
|
+
attemptGroupRelocation: function(t1, t2, subtrees, route) {
|
811
|
+
/* Either t1.childNodes and t2.childNodes have the same length, or
|
812
|
+
* there are at least two groups of similar elements can be found.
|
813
|
+
* attempts are made at equalizing t1 with t2. First all initial
|
814
|
+
* elements with no group affiliation (gaps=true) are removed (if
|
815
|
+
* only in t1) or added (if only in t2). Then the creation of a group
|
816
|
+
* relocation diff is attempted.
|
817
|
+
*/
|
818
|
+
var t = this;
|
819
|
+
var gapInformation = getGapInformation(t1, t2, subtrees),
|
820
|
+
gaps1 = gapInformation.gaps1,
|
821
|
+
gaps2 = gapInformation.gaps2,
|
822
|
+
shortest = Math.min(gaps1.length, gaps2.length),
|
823
|
+
destinationDifferent, toGroup,
|
824
|
+
group, node, similarNode, testI, diffs = [],
|
825
|
+
index1, index2, j;
|
826
|
+
|
827
|
+
|
828
|
+
for (index2 = 0, index1 = 0; index2 < shortest; index1 += 1, index2 += 1) {
|
829
|
+
if (gaps1[index2] === true) {
|
830
|
+
node = t1.childNodes[index1];
|
831
|
+
if (node.nodeName === '#text') {
|
832
|
+
if (t2.childNodes[index2].nodeName === '#text' && node.data !== t2.childNodes[index2].data) {
|
833
|
+
testI = index1;
|
834
|
+
while (t1.childNodes.length > testI + 1 && t1.childNodes[testI + 1].nodeName === '#text') {
|
835
|
+
testI += 1;
|
836
|
+
if (t2.childNodes[index2].data === t1.childNodes[testI].data) {
|
837
|
+
similarNode = true;
|
838
|
+
break;
|
839
|
+
}
|
840
|
+
}
|
841
|
+
if (!similarNode) {
|
842
|
+
diffs.push(new Diff()
|
843
|
+
.setValue(t._const.action, t._const.modifyTextElement)
|
844
|
+
.setValue(t._const.route, route.concat(index2))
|
845
|
+
.setValue(t._const.oldValue, node.data)
|
846
|
+
.setValue(t._const.newValue, t2.childNodes[index2].data)
|
847
|
+
);
|
848
|
+
return diffs;
|
849
|
+
}
|
850
|
+
}
|
851
|
+
diffs.push(new Diff()
|
852
|
+
.setValue(t._const.action, t._const.removeTextElement)
|
853
|
+
.setValue(t._const.route, route.concat(index2))
|
854
|
+
.setValue(t._const.value, node.data)
|
855
|
+
);
|
856
|
+
gaps1.splice(index2, 1);
|
857
|
+
shortest = Math.min(gaps1.length, gaps2.length);
|
858
|
+
index2 -= 1;
|
859
|
+
} else {
|
860
|
+
diffs.push(new Diff()
|
861
|
+
.setValue(t._const.action, t._const.removeElement)
|
862
|
+
.setValue(t._const.route, route.concat(index2))
|
863
|
+
.setValue(t._const.element, cloneObj(node))
|
864
|
+
);
|
865
|
+
gaps1.splice(index2, 1);
|
866
|
+
shortest = Math.min(gaps1.length, gaps2.length);
|
867
|
+
index2 -= 1;
|
868
|
+
}
|
869
|
+
|
870
|
+
} else if (gaps2[index2] === true) {
|
871
|
+
node = t2.childNodes[index2];
|
872
|
+
if (node.nodeName === '#text') {
|
873
|
+
diffs.push(new Diff()
|
874
|
+
.setValue(t._const.action, t._const.addTextElement)
|
875
|
+
.setValue(t._const.route, route.concat(index2))
|
876
|
+
.setValue(t._const.value, node.data)
|
877
|
+
);
|
878
|
+
gaps1.splice(index2, 0, true);
|
879
|
+
shortest = Math.min(gaps1.length, gaps2.length);
|
880
|
+
index1 -= 1;
|
881
|
+
} else {
|
882
|
+
diffs.push(new Diff()
|
883
|
+
.setValue(t._const.action, t._const.addElement)
|
884
|
+
.setValue(t._const.route, route.concat(index2))
|
885
|
+
.setValue(t._const.element, cloneObj(node))
|
886
|
+
);
|
887
|
+
gaps1.splice(index2, 0, true);
|
888
|
+
shortest = Math.min(gaps1.length, gaps2.length);
|
889
|
+
index1 -= 1;
|
890
|
+
}
|
891
|
+
|
892
|
+
} else if (gaps1[index2] !== gaps2[index2]) {
|
893
|
+
if (diffs.length > 0) {
|
894
|
+
return diffs;
|
895
|
+
}
|
896
|
+
// group relocation
|
897
|
+
group = subtrees[gaps1[index2]];
|
898
|
+
toGroup = Math.min(group.newValue, (t1.childNodes.length - group.length));
|
899
|
+
if (toGroup !== group.oldValue) {
|
900
|
+
// Check whether destination nodes are different than originating ones.
|
901
|
+
destinationDifferent = false;
|
902
|
+
for (j = 0; j < group.length; j += 1) {
|
903
|
+
if (!roughlyEqual(t1.childNodes[toGroup + j], t1.childNodes[group.oldValue + j], [], false, true)) {
|
904
|
+
destinationDifferent = true;
|
905
|
+
}
|
906
|
+
}
|
907
|
+
if (destinationDifferent) {
|
908
|
+
return [new Diff()
|
909
|
+
.setValue(t._const.action, t._const.relocateGroup)
|
910
|
+
.setValue('groupLength', group.length)
|
911
|
+
.setValue(t._const.from, group.oldValue)
|
912
|
+
.setValue(t._const.to, toGroup)
|
913
|
+
.setValue(t._const.route, route)
|
914
|
+
];
|
915
|
+
}
|
916
|
+
}
|
917
|
+
}
|
918
|
+
}
|
919
|
+
return diffs;
|
920
|
+
},
|
921
|
+
|
922
|
+
findValueDiff: function(t1, t2, route) {
|
923
|
+
// Differences of value. Only useful if the value/selection/checked value
|
924
|
+
// differs from what is represented in the DOM. For example in the case
|
925
|
+
// of filled out forms, etc.
|
926
|
+
var diffs = [];
|
927
|
+
var t = this;
|
928
|
+
|
929
|
+
if (t1.selected !== t2.selected) {
|
930
|
+
diffs.push(new Diff()
|
931
|
+
.setValue(t._const.action, t._const.modifySelected)
|
932
|
+
.setValue(t._const.oldValue, t1.selected)
|
933
|
+
.setValue(t._const.newValue, t2.selected)
|
934
|
+
.setValue(t._const.route, route)
|
935
|
+
);
|
936
|
+
}
|
937
|
+
|
938
|
+
if ((t1.value || t2.value) && t1.value !== t2.value && t1.nodeName !== 'OPTION') {
|
939
|
+
diffs.push(new Diff()
|
940
|
+
.setValue(t._const.action, t._const.modifyValue)
|
941
|
+
.setValue(t._const.oldValue, t1.value)
|
942
|
+
.setValue(t._const.newValue, t2.value)
|
943
|
+
.setValue(t._const.route, route)
|
944
|
+
);
|
945
|
+
}
|
946
|
+
if (t1.checked !== t2.checked) {
|
947
|
+
diffs.push(new Diff()
|
948
|
+
.setValue(t._const.action, t._const.modifyChecked)
|
949
|
+
.setValue(t._const.oldValue, t1.checked)
|
950
|
+
.setValue(t._const.newValue, t2.checked)
|
951
|
+
.setValue(t._const.route, route)
|
952
|
+
);
|
953
|
+
}
|
954
|
+
|
955
|
+
return diffs;
|
956
|
+
},
|
957
|
+
|
958
|
+
// ===== Apply a virtual diff =====
|
959
|
+
|
960
|
+
applyVirtual: function(tree, diffs) {
|
961
|
+
var dobj = this;
|
962
|
+
if (diffs.length === 0) {
|
963
|
+
return true;
|
964
|
+
}
|
965
|
+
diffs.forEach(function(diff) {
|
966
|
+
dobj.applyVirtualDiff(tree, diff);
|
967
|
+
});
|
968
|
+
return true;
|
969
|
+
},
|
970
|
+
getFromVirtualRoute: function(tree, route) {
|
971
|
+
var node = tree,
|
972
|
+
parentNode, nodeIndex;
|
973
|
+
|
974
|
+
route = route.slice();
|
975
|
+
while (route.length > 0) {
|
976
|
+
if (!node.childNodes) {
|
977
|
+
return false;
|
978
|
+
}
|
979
|
+
nodeIndex = route.splice(0, 1)[0];
|
980
|
+
parentNode = node;
|
981
|
+
node = node.childNodes[nodeIndex];
|
982
|
+
}
|
983
|
+
return {
|
984
|
+
node: node,
|
985
|
+
parentNode: parentNode,
|
986
|
+
nodeIndex: nodeIndex
|
987
|
+
};
|
988
|
+
},
|
989
|
+
applyVirtualDiff: function(tree, diff) {
|
990
|
+
var routeInfo = this.getFromVirtualRoute(tree, diff[this._const.route]),
|
991
|
+
node = routeInfo.node,
|
992
|
+
parentNode = routeInfo.parentNode,
|
993
|
+
nodeIndex = routeInfo.nodeIndex,
|
994
|
+
newNode, route, c;
|
995
|
+
|
996
|
+
var t = this;
|
997
|
+
// pre-diff hook
|
998
|
+
var info = {
|
999
|
+
diff: diff,
|
1000
|
+
node: node
|
1001
|
+
};
|
1002
|
+
|
1003
|
+
if (this.preVirtualDiffApply(info)) {
|
1004
|
+
return true;
|
1005
|
+
}
|
1006
|
+
|
1007
|
+
switch (diff[this._const.action]) {
|
1008
|
+
case this._const.addAttribute:
|
1009
|
+
if (!node.attributes) {
|
1010
|
+
node.attributes = {};
|
1011
|
+
}
|
1012
|
+
|
1013
|
+
node.attributes[diff[this._const.name]] = diff[this._const.value];
|
1014
|
+
|
1015
|
+
if (diff[this._const.name] === 'checked') {
|
1016
|
+
node.checked = true;
|
1017
|
+
} else if (diff[this._const.name] === 'selected') {
|
1018
|
+
node.selected = true;
|
1019
|
+
} else if (node.nodeName === 'INPUT' && diff[this._const.name] === 'value') {
|
1020
|
+
node.value = diff[this._const.value];
|
1021
|
+
}
|
1022
|
+
|
1023
|
+
break;
|
1024
|
+
case this._const.modifyAttribute:
|
1025
|
+
node.attributes[diff[this._const.name]] = diff[this._const.newValue];
|
1026
|
+
if (node.nodeName === 'INPUT' && diff[this._const.name] === 'value') {
|
1027
|
+
node.value = diff[this._const.value];
|
1028
|
+
}
|
1029
|
+
break;
|
1030
|
+
case this._const.removeAttribute:
|
1031
|
+
|
1032
|
+
delete node.attributes[diff[this._const.name]];
|
1033
|
+
|
1034
|
+
if (Object.keys(node.attributes).length === 0) {
|
1035
|
+
delete node.attributes;
|
1036
|
+
}
|
1037
|
+
|
1038
|
+
if (diff[this._const.name] === 'checked') {
|
1039
|
+
node.checked = false;
|
1040
|
+
} else if (diff[this._const.name] === 'selected') {
|
1041
|
+
delete node.selected;
|
1042
|
+
} else if (node.nodeName === 'INPUT' && diff[this._const.name] === 'value') {
|
1043
|
+
delete node.value;
|
1044
|
+
}
|
1045
|
+
|
1046
|
+
break;
|
1047
|
+
case this._const.modifyTextElement:
|
1048
|
+
node.data = diff[this._const.newValue];
|
1049
|
+
|
1050
|
+
if (parentNode.nodeName === 'TEXTAREA') {
|
1051
|
+
parentNode.value = diff[this._const.newValue];
|
1052
|
+
}
|
1053
|
+
break;
|
1054
|
+
case this._const.modifyValue:
|
1055
|
+
node.value = diff[this._const.newValue];
|
1056
|
+
break;
|
1057
|
+
case this._const.modifyComment:
|
1058
|
+
node.data = diff[this._const.newValue];
|
1059
|
+
break;
|
1060
|
+
case this._const.modifyChecked:
|
1061
|
+
node.checked = diff[this._const.newValue];
|
1062
|
+
break;
|
1063
|
+
case this._const.modifySelected:
|
1064
|
+
node.selected = diff[this._const.newValue];
|
1065
|
+
break;
|
1066
|
+
case this._const.replaceElement:
|
1067
|
+
newNode = cloneObj(diff[this._const.newValue]);
|
1068
|
+
newNode.outerDone = true;
|
1069
|
+
newNode.innerDone = true;
|
1070
|
+
newNode.valueDone = true;
|
1071
|
+
parentNode.childNodes[nodeIndex] = newNode;
|
1072
|
+
break;
|
1073
|
+
case this._const.relocateGroup:
|
1074
|
+
node.childNodes.splice(diff[this._const.from], diff.groupLength).reverse()
|
1075
|
+
.forEach(function(movedNode) {
|
1076
|
+
node.childNodes.splice(diff[t._const.to], 0, movedNode);
|
1077
|
+
});
|
1078
|
+
break;
|
1079
|
+
case this._const.removeElement:
|
1080
|
+
parentNode.childNodes.splice(nodeIndex, 1);
|
1081
|
+
break;
|
1082
|
+
case this._const.addElement:
|
1083
|
+
route = diff[this._const.route].slice();
|
1084
|
+
c = route.splice(route.length - 1, 1)[0];
|
1085
|
+
node = this.getFromVirtualRoute(tree, route).node;
|
1086
|
+
newNode = cloneObj(diff[this._const.element]);
|
1087
|
+
newNode.outerDone = true;
|
1088
|
+
newNode.innerDone = true;
|
1089
|
+
newNode.valueDone = true;
|
1090
|
+
|
1091
|
+
if (!node.childNodes) {
|
1092
|
+
node.childNodes = [];
|
1093
|
+
}
|
1094
|
+
|
1095
|
+
if (c >= node.childNodes.length) {
|
1096
|
+
node.childNodes.push(newNode);
|
1097
|
+
} else {
|
1098
|
+
node.childNodes.splice(c, 0, newNode);
|
1099
|
+
}
|
1100
|
+
break;
|
1101
|
+
case this._const.removeTextElement:
|
1102
|
+
parentNode.childNodes.splice(nodeIndex, 1);
|
1103
|
+
if (parentNode.nodeName === 'TEXTAREA') {
|
1104
|
+
delete parentNode.value;
|
1105
|
+
}
|
1106
|
+
break;
|
1107
|
+
case this._const.addTextElement:
|
1108
|
+
route = diff[this._const.route].slice();
|
1109
|
+
c = route.splice(route.length - 1, 1)[0];
|
1110
|
+
newNode = {};
|
1111
|
+
newNode.nodeName = '#text';
|
1112
|
+
newNode.data = diff[this._const.value];
|
1113
|
+
node = this.getFromVirtualRoute(tree, route).node;
|
1114
|
+
if (!node.childNodes) {
|
1115
|
+
node.childNodes = [];
|
1116
|
+
}
|
1117
|
+
|
1118
|
+
if (c >= node.childNodes.length) {
|
1119
|
+
node.childNodes.push(newNode);
|
1120
|
+
} else {
|
1121
|
+
node.childNodes.splice(c, 0, newNode);
|
1122
|
+
}
|
1123
|
+
if (node.nodeName === 'TEXTAREA') {
|
1124
|
+
node.value = diff[this._const.newValue];
|
1125
|
+
}
|
1126
|
+
break;
|
1127
|
+
default:
|
1128
|
+
console.log('unknown action');
|
1129
|
+
}
|
1130
|
+
|
1131
|
+
// capture newNode for the callback
|
1132
|
+
info.newNode = newNode;
|
1133
|
+
this.postVirtualDiffApply(info);
|
1134
|
+
|
1135
|
+
return;
|
1136
|
+
},
|
1137
|
+
|
1138
|
+
|
1139
|
+
|
1140
|
+
|
1141
|
+
// ===== Apply a diff =====
|
1142
|
+
|
1143
|
+
apply: function(tree, diffs) {
|
1144
|
+
var dobj = this;
|
1145
|
+
|
1146
|
+
if (diffs.length === 0) {
|
1147
|
+
return true;
|
1148
|
+
}
|
1149
|
+
diffs.forEach(function(diff) {
|
1150
|
+
if (!dobj.applyDiff(tree, diff)) {
|
1151
|
+
return false;
|
1152
|
+
}
|
1153
|
+
});
|
1154
|
+
return true;
|
1155
|
+
},
|
1156
|
+
getFromRoute: function(tree, route) {
|
1157
|
+
route = route.slice();
|
1158
|
+
var c, node = tree;
|
1159
|
+
while (route.length > 0) {
|
1160
|
+
if (!node.childNodes) {
|
1161
|
+
return false;
|
1162
|
+
}
|
1163
|
+
c = route.splice(0, 1)[0];
|
1164
|
+
node = node.childNodes[c];
|
1165
|
+
}
|
1166
|
+
return node;
|
1167
|
+
},
|
1168
|
+
applyDiff: function(tree, diff) {
|
1169
|
+
var node = this.getFromRoute(tree, diff[this._const.route]),
|
1170
|
+
newNode, reference, route, c;
|
1171
|
+
|
1172
|
+
var t = this;
|
1173
|
+
// pre-diff hook
|
1174
|
+
var info = {
|
1175
|
+
diff: diff,
|
1176
|
+
node: node
|
1177
|
+
};
|
1178
|
+
|
1179
|
+
if (this.preDiffApply(info)) {
|
1180
|
+
return true;
|
1181
|
+
}
|
1182
|
+
|
1183
|
+
switch (diff[this._const.action]) {
|
1184
|
+
case this._const.addAttribute:
|
1185
|
+
if (!node || !node.setAttribute) {
|
1186
|
+
return false;
|
1187
|
+
}
|
1188
|
+
node.setAttribute(diff[this._const.name], diff[this._const.value]);
|
1189
|
+
break;
|
1190
|
+
case this._const.modifyAttribute:
|
1191
|
+
if (!node || !node.setAttribute) {
|
1192
|
+
return false;
|
1193
|
+
}
|
1194
|
+
node.setAttribute(diff[this._const.name], diff[this._const.newValue]);
|
1195
|
+
break;
|
1196
|
+
case this._const.removeAttribute:
|
1197
|
+
if (!node || !node.removeAttribute) {
|
1198
|
+
return false;
|
1199
|
+
}
|
1200
|
+
node.removeAttribute(diff[this._const.name]);
|
1201
|
+
break;
|
1202
|
+
case this._const.modifyTextElement:
|
1203
|
+
if (!node || node.nodeType !== 3) {
|
1204
|
+
return false;
|
1205
|
+
}
|
1206
|
+
this.textDiff(node, node.data, diff[this._const.oldValue], diff[this._const.newValue]);
|
1207
|
+
break;
|
1208
|
+
case this._const.modifyValue:
|
1209
|
+
if (!node || typeof node.value === 'undefined') {
|
1210
|
+
return false;
|
1211
|
+
}
|
1212
|
+
node.value = diff[this._const.newValue];
|
1213
|
+
break;
|
1214
|
+
case this._const.modifyComment:
|
1215
|
+
if (!node || typeof node.data === 'undefined') {
|
1216
|
+
return false;
|
1217
|
+
}
|
1218
|
+
this.textDiff(node, node.data, diff[this._const.oldValue], diff[this._const.newValue]);
|
1219
|
+
break;
|
1220
|
+
case this._const.modifyChecked:
|
1221
|
+
if (!node || typeof node.checked === 'undefined') {
|
1222
|
+
return false;
|
1223
|
+
}
|
1224
|
+
node.checked = diff[this._const.newValue];
|
1225
|
+
break;
|
1226
|
+
case this._const.modifySelected:
|
1227
|
+
if (!node || typeof node.selected === 'undefined') {
|
1228
|
+
return false;
|
1229
|
+
}
|
1230
|
+
node.selected = diff[this._const.newValue];
|
1231
|
+
break;
|
1232
|
+
case this._const.replaceElement:
|
1233
|
+
node.parentNode.replaceChild(this.objToNode(diff[this._const.newValue], node.namespaceURI === 'http://www.w3.org/2000/svg'), node);
|
1234
|
+
break;
|
1235
|
+
case this._const.relocateGroup:
|
1236
|
+
Array.apply(null, new Array(diff.groupLength)).map(function() {
|
1237
|
+
return node.removeChild(node.childNodes[diff[t._const.from]]);
|
1238
|
+
}).forEach(function(childNode, index) {
|
1239
|
+
if (index === 0) {
|
1240
|
+
reference = node.childNodes[diff[t._const.to]];
|
1241
|
+
}
|
1242
|
+
node.insertBefore(childNode, reference);
|
1243
|
+
});
|
1244
|
+
break;
|
1245
|
+
case this._const.removeElement:
|
1246
|
+
node.parentNode.removeChild(node);
|
1247
|
+
break;
|
1248
|
+
case this._const.addElement:
|
1249
|
+
route = diff[this._const.route].slice();
|
1250
|
+
c = route.splice(route.length - 1, 1)[0];
|
1251
|
+
node = this.getFromRoute(tree, route);
|
1252
|
+
node.insertBefore(this.objToNode(diff[this._const.element], node.namespaceURI === 'http://www.w3.org/2000/svg'), node.childNodes[c]);
|
1253
|
+
break;
|
1254
|
+
case this._const.removeTextElement:
|
1255
|
+
if (!node || node.nodeType !== 3) {
|
1256
|
+
return false;
|
1257
|
+
}
|
1258
|
+
node.parentNode.removeChild(node);
|
1259
|
+
break;
|
1260
|
+
case this._const.addTextElement:
|
1261
|
+
route = diff[this._const.route].slice();
|
1262
|
+
c = route.splice(route.length - 1, 1)[0];
|
1263
|
+
newNode = document.createTextNode(diff[this._const.value]);
|
1264
|
+
node = this.getFromRoute(tree, route);
|
1265
|
+
if (!node || !node.childNodes) {
|
1266
|
+
return false;
|
1267
|
+
}
|
1268
|
+
node.insertBefore(newNode, node.childNodes[c]);
|
1269
|
+
break;
|
1270
|
+
default:
|
1271
|
+
console.log('unknown action');
|
1272
|
+
}
|
1273
|
+
|
1274
|
+
// if a new node was created, we might be interested in it
|
1275
|
+
// post diff hook
|
1276
|
+
info.newNode = newNode;
|
1277
|
+
this.postDiffApply(info);
|
1278
|
+
|
1279
|
+
return true;
|
1280
|
+
},
|
1281
|
+
|
1282
|
+
// ===== Undo a diff =====
|
1283
|
+
|
1284
|
+
undo: function(tree, diffs) {
|
1285
|
+
diffs = diffs.slice();
|
1286
|
+
var dobj = this;
|
1287
|
+
if (!diffs.length) {
|
1288
|
+
diffs = [diffs];
|
1289
|
+
}
|
1290
|
+
diffs.reverse();
|
1291
|
+
diffs.forEach(function(diff) {
|
1292
|
+
dobj.undoDiff(tree, diff);
|
1293
|
+
});
|
1294
|
+
},
|
1295
|
+
undoDiff: function(tree, diff) {
|
1296
|
+
|
1297
|
+
switch (diff[this._const.action]) {
|
1298
|
+
case this._const.addAttribute:
|
1299
|
+
diff[this._const.action] = this._const.removeAttribute;
|
1300
|
+
this.applyDiff(tree, diff);
|
1301
|
+
break;
|
1302
|
+
case this._const.modifyAttribute:
|
1303
|
+
swap(diff, this._const.oldValue, this._const.newValue);
|
1304
|
+
this.applyDiff(tree, diff);
|
1305
|
+
break;
|
1306
|
+
case this._const.removeAttribute:
|
1307
|
+
diff[this._const.action] = this._const.addAttribute;
|
1308
|
+
this.applyDiff(tree, diff);
|
1309
|
+
break;
|
1310
|
+
case this._const.modifyTextElement:
|
1311
|
+
swap(diff, this._const.oldValue, this._const.newValue);
|
1312
|
+
this.applyDiff(tree, diff);
|
1313
|
+
break;
|
1314
|
+
case this._const.modifyValue:
|
1315
|
+
swap(diff, this._const.oldValue, this._const.newValue);
|
1316
|
+
this.applyDiff(tree, diff);
|
1317
|
+
break;
|
1318
|
+
case this._const.modifyComment:
|
1319
|
+
swap(diff, this._const.oldValue, this._const.newValue);
|
1320
|
+
this.applyDiff(tree, diff);
|
1321
|
+
break;
|
1322
|
+
case this._const.modifyChecked:
|
1323
|
+
swap(diff, this._const.oldValue, this._const.newValue);
|
1324
|
+
this.applyDiff(tree, diff);
|
1325
|
+
break;
|
1326
|
+
case this._const.modifySelected:
|
1327
|
+
swap(diff, this._const.oldValue, this._const.newValue);
|
1328
|
+
this.applyDiff(tree, diff);
|
1329
|
+
break;
|
1330
|
+
case this._const.replaceElement:
|
1331
|
+
swap(diff, this._const.oldValue, this._const.newValue);
|
1332
|
+
this.applyDiff(tree, diff);
|
1333
|
+
break;
|
1334
|
+
case this._const.relocateGroup:
|
1335
|
+
swap(diff, this._const.from, this._const.to);
|
1336
|
+
this.applyDiff(tree, diff);
|
1337
|
+
break;
|
1338
|
+
case this._const.removeElement:
|
1339
|
+
diff[this._const.action] = this._const.addElement;
|
1340
|
+
this.applyDiff(tree, diff);
|
1341
|
+
break;
|
1342
|
+
case this._const.addElement:
|
1343
|
+
diff[this._const.action] = this._const.removeElement;
|
1344
|
+
this.applyDiff(tree, diff);
|
1345
|
+
break;
|
1346
|
+
case this._const.removeTextElement:
|
1347
|
+
diff[this._const.action] = this._const.addTextElement;
|
1348
|
+
this.applyDiff(tree, diff);
|
1349
|
+
break;
|
1350
|
+
case this._const.addTextElement:
|
1351
|
+
diff[this._const.action] = this._const.removeTextElement;
|
1352
|
+
this.applyDiff(tree, diff);
|
1353
|
+
break;
|
1354
|
+
default:
|
1355
|
+
console.log('unknown action');
|
1356
|
+
}
|
1357
|
+
|
1358
|
+
}
|
1359
|
+
};
|
1360
|
+
|
1361
|
+
if (typeof exports !== 'undefined') {
|
1362
|
+
if (typeof module !== 'undefined' && module.exports) {
|
1363
|
+
exports = module.exports = diffDOM;
|
1364
|
+
}
|
1365
|
+
exports.diffDOM = diffDOM;
|
1366
|
+
} else {
|
1367
|
+
// `window` in the browser, or `exports` on the server
|
1368
|
+
this.diffDOM = diffDOM;
|
1369
|
+
}
|
1370
|
+
|
1371
|
+
}.call(this));
|