lively 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,763 @@
1
+ (function (global, factory) {
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
3
+ typeof define === 'function' && define.amd ? define(factory) :
4
+ (global = global || self, global.morphdom = factory());
5
+ }(this, function () { 'use strict';
6
+
7
+ var DOCUMENT_FRAGMENT_NODE = 11;
8
+
9
+ function morphAttrs(fromNode, toNode) {
10
+ var toNodeAttrs = toNode.attributes;
11
+ var attr;
12
+ var attrName;
13
+ var attrNamespaceURI;
14
+ var attrValue;
15
+ var fromValue;
16
+
17
+ // document-fragments dont have attributes so lets not do anything
18
+ if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE || fromNode.nodeType === DOCUMENT_FRAGMENT_NODE) {
19
+ return;
20
+ }
21
+
22
+ // update attributes on original DOM element
23
+ for (var i = toNodeAttrs.length - 1; i >= 0; i--) {
24
+ attr = toNodeAttrs[i];
25
+ attrName = attr.name;
26
+ attrNamespaceURI = attr.namespaceURI;
27
+ attrValue = attr.value;
28
+
29
+ if (attrNamespaceURI) {
30
+ attrName = attr.localName || attrName;
31
+ fromValue = fromNode.getAttributeNS(attrNamespaceURI, attrName);
32
+
33
+ if (fromValue !== attrValue) {
34
+ if (attr.prefix === 'xmlns'){
35
+ attrName = attr.name; // It's not allowed to set an attribute with the XMLNS namespace without specifying the `xmlns` prefix
36
+ }
37
+ fromNode.setAttributeNS(attrNamespaceURI, attrName, attrValue);
38
+ }
39
+ } else {
40
+ fromValue = fromNode.getAttribute(attrName);
41
+
42
+ if (fromValue !== attrValue) {
43
+ fromNode.setAttribute(attrName, attrValue);
44
+ }
45
+ }
46
+ }
47
+
48
+ // Remove any extra attributes found on the original DOM element that
49
+ // weren't found on the target element.
50
+ var fromNodeAttrs = fromNode.attributes;
51
+
52
+ for (var d = fromNodeAttrs.length - 1; d >= 0; d--) {
53
+ attr = fromNodeAttrs[d];
54
+ attrName = attr.name;
55
+ attrNamespaceURI = attr.namespaceURI;
56
+
57
+ if (attrNamespaceURI) {
58
+ attrName = attr.localName || attrName;
59
+
60
+ if (!toNode.hasAttributeNS(attrNamespaceURI, attrName)) {
61
+ fromNode.removeAttributeNS(attrNamespaceURI, attrName);
62
+ }
63
+ } else {
64
+ if (!toNode.hasAttribute(attrName)) {
65
+ fromNode.removeAttribute(attrName);
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ var range; // Create a range object for efficently rendering strings to elements.
72
+ var NS_XHTML = 'http://www.w3.org/1999/xhtml';
73
+
74
+ var doc = typeof document === 'undefined' ? undefined : document;
75
+ var HAS_TEMPLATE_SUPPORT = !!doc && 'content' in doc.createElement('template');
76
+ var HAS_RANGE_SUPPORT = !!doc && doc.createRange && 'createContextualFragment' in doc.createRange();
77
+
78
+ function createFragmentFromTemplate(str) {
79
+ var template = doc.createElement('template');
80
+ template.innerHTML = str;
81
+ return template.content.childNodes[0];
82
+ }
83
+
84
+ function createFragmentFromRange(str) {
85
+ if (!range) {
86
+ range = doc.createRange();
87
+ range.selectNode(doc.body);
88
+ }
89
+
90
+ var fragment = range.createContextualFragment(str);
91
+ return fragment.childNodes[0];
92
+ }
93
+
94
+ function createFragmentFromWrap(str) {
95
+ var fragment = doc.createElement('body');
96
+ fragment.innerHTML = str;
97
+ return fragment.childNodes[0];
98
+ }
99
+
100
+ /**
101
+ * This is about the same
102
+ * var html = new DOMParser().parseFromString(str, 'text/html');
103
+ * return html.body.firstChild;
104
+ *
105
+ * @method toElement
106
+ * @param {String} str
107
+ */
108
+ function toElement(str) {
109
+ str = str.trim();
110
+ if (HAS_TEMPLATE_SUPPORT) {
111
+ // avoid restrictions on content for things like `<tr><th>Hi</th></tr>` which
112
+ // createContextualFragment doesn't support
113
+ // <template> support not available in IE
114
+ return createFragmentFromTemplate(str);
115
+ } else if (HAS_RANGE_SUPPORT) {
116
+ return createFragmentFromRange(str);
117
+ }
118
+
119
+ return createFragmentFromWrap(str);
120
+ }
121
+
122
+ /**
123
+ * Returns true if two node's names are the same.
124
+ *
125
+ * NOTE: We don't bother checking `namespaceURI` because you will never find two HTML elements with the same
126
+ * nodeName and different namespace URIs.
127
+ *
128
+ * @param {Element} a
129
+ * @param {Element} b The target element
130
+ * @return {boolean}
131
+ */
132
+ function compareNodeNames(fromEl, toEl) {
133
+ var fromNodeName = fromEl.nodeName;
134
+ var toNodeName = toEl.nodeName;
135
+ var fromCodeStart, toCodeStart;
136
+
137
+ if (fromNodeName === toNodeName) {
138
+ return true;
139
+ }
140
+
141
+ fromCodeStart = fromNodeName.charCodeAt(0);
142
+ toCodeStart = toNodeName.charCodeAt(0);
143
+
144
+ // If the target element is a virtual DOM node or SVG node then we may
145
+ // need to normalize the tag name before comparing. Normal HTML elements that are
146
+ // in the "http://www.w3.org/1999/xhtml"
147
+ // are converted to upper case
148
+ if (fromCodeStart <= 90 && toCodeStart >= 97) { // from is upper and to is lower
149
+ return fromNodeName === toNodeName.toUpperCase();
150
+ } else if (toCodeStart <= 90 && fromCodeStart >= 97) { // to is upper and from is lower
151
+ return toNodeName === fromNodeName.toUpperCase();
152
+ } else {
153
+ return false;
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Create an element, optionally with a known namespace URI.
159
+ *
160
+ * @param {string} name the element name, e.g. 'div' or 'svg'
161
+ * @param {string} [namespaceURI] the element's namespace URI, i.e. the value of
162
+ * its `xmlns` attribute or its inferred namespace.
163
+ *
164
+ * @return {Element}
165
+ */
166
+ function createElementNS(name, namespaceURI) {
167
+ return !namespaceURI || namespaceURI === NS_XHTML ?
168
+ doc.createElement(name) :
169
+ doc.createElementNS(namespaceURI, name);
170
+ }
171
+
172
+ /**
173
+ * Copies the children of one DOM element to another DOM element
174
+ */
175
+ function moveChildren(fromEl, toEl) {
176
+ var curChild = fromEl.firstChild;
177
+ while (curChild) {
178
+ var nextChild = curChild.nextSibling;
179
+ toEl.appendChild(curChild);
180
+ curChild = nextChild;
181
+ }
182
+ return toEl;
183
+ }
184
+
185
+ function syncBooleanAttrProp(fromEl, toEl, name) {
186
+ if (fromEl[name] !== toEl[name]) {
187
+ fromEl[name] = toEl[name];
188
+ if (fromEl[name]) {
189
+ fromEl.setAttribute(name, '');
190
+ } else {
191
+ fromEl.removeAttribute(name);
192
+ }
193
+ }
194
+ }
195
+
196
+ var specialElHandlers = {
197
+ OPTION: function(fromEl, toEl) {
198
+ var parentNode = fromEl.parentNode;
199
+ if (parentNode) {
200
+ var parentName = parentNode.nodeName.toUpperCase();
201
+ if (parentName === 'OPTGROUP') {
202
+ parentNode = parentNode.parentNode;
203
+ parentName = parentNode && parentNode.nodeName.toUpperCase();
204
+ }
205
+ if (parentName === 'SELECT' && !parentNode.hasAttribute('multiple')) {
206
+ if (fromEl.hasAttribute('selected') && !toEl.selected) {
207
+ // Workaround for MS Edge bug where the 'selected' attribute can only be
208
+ // removed if set to a non-empty value:
209
+ // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12087679/
210
+ fromEl.setAttribute('selected', 'selected');
211
+ fromEl.removeAttribute('selected');
212
+ }
213
+ // We have to reset select element's selectedIndex to -1, otherwise setting
214
+ // fromEl.selected using the syncBooleanAttrProp below has no effect.
215
+ // The correct selectedIndex will be set in the SELECT special handler below.
216
+ parentNode.selectedIndex = -1;
217
+ }
218
+ }
219
+ syncBooleanAttrProp(fromEl, toEl, 'selected');
220
+ },
221
+ /**
222
+ * The "value" attribute is special for the <input> element since it sets
223
+ * the initial value. Changing the "value" attribute without changing the
224
+ * "value" property will have no effect since it is only used to the set the
225
+ * initial value. Similar for the "checked" attribute, and "disabled".
226
+ */
227
+ INPUT: function(fromEl, toEl) {
228
+ syncBooleanAttrProp(fromEl, toEl, 'checked');
229
+ syncBooleanAttrProp(fromEl, toEl, 'disabled');
230
+
231
+ if (fromEl.value !== toEl.value) {
232
+ fromEl.value = toEl.value;
233
+ }
234
+
235
+ if (!toEl.hasAttribute('value')) {
236
+ fromEl.removeAttribute('value');
237
+ }
238
+ },
239
+
240
+ TEXTAREA: function(fromEl, toEl) {
241
+ var newValue = toEl.value;
242
+ if (fromEl.value !== newValue) {
243
+ fromEl.value = newValue;
244
+ }
245
+
246
+ var firstChild = fromEl.firstChild;
247
+ if (firstChild) {
248
+ // Needed for IE. Apparently IE sets the placeholder as the
249
+ // node value and vise versa. This ignores an empty update.
250
+ var oldValue = firstChild.nodeValue;
251
+
252
+ if (oldValue == newValue || (!newValue && oldValue == fromEl.placeholder)) {
253
+ return;
254
+ }
255
+
256
+ firstChild.nodeValue = newValue;
257
+ }
258
+ },
259
+ SELECT: function(fromEl, toEl) {
260
+ if (!toEl.hasAttribute('multiple')) {
261
+ var selectedIndex = -1;
262
+ var i = 0;
263
+ // We have to loop through children of fromEl, not toEl since nodes can be moved
264
+ // from toEl to fromEl directly when morphing.
265
+ // At the time this special handler is invoked, all children have already been morphed
266
+ // and appended to / removed from fromEl, so using fromEl here is safe and correct.
267
+ var curChild = fromEl.firstChild;
268
+ var optgroup;
269
+ var nodeName;
270
+ while(curChild) {
271
+ nodeName = curChild.nodeName && curChild.nodeName.toUpperCase();
272
+ if (nodeName === 'OPTGROUP') {
273
+ optgroup = curChild;
274
+ curChild = optgroup.firstChild;
275
+ } else {
276
+ if (nodeName === 'OPTION') {
277
+ if (curChild.hasAttribute('selected')) {
278
+ selectedIndex = i;
279
+ break;
280
+ }
281
+ i++;
282
+ }
283
+ curChild = curChild.nextSibling;
284
+ if (!curChild && optgroup) {
285
+ curChild = optgroup.nextSibling;
286
+ optgroup = null;
287
+ }
288
+ }
289
+ }
290
+
291
+ fromEl.selectedIndex = selectedIndex;
292
+ }
293
+ }
294
+ };
295
+
296
+ var ELEMENT_NODE = 1;
297
+ var DOCUMENT_FRAGMENT_NODE$1 = 11;
298
+ var TEXT_NODE = 3;
299
+ var COMMENT_NODE = 8;
300
+
301
+ function noop() {}
302
+
303
+ function defaultGetNodeKey(node) {
304
+ if (node) {
305
+ return (node.getAttribute && node.getAttribute('id')) || node.id;
306
+ }
307
+ }
308
+
309
+ function morphdomFactory(morphAttrs) {
310
+
311
+ return function morphdom(fromNode, toNode, options) {
312
+ if (!options) {
313
+ options = {};
314
+ }
315
+
316
+ if (typeof toNode === 'string') {
317
+ if (fromNode.nodeName === '#document' || fromNode.nodeName === 'HTML' || fromNode.nodeName === 'BODY') {
318
+ var toNodeHtml = toNode;
319
+ toNode = doc.createElement('html');
320
+ toNode.innerHTML = toNodeHtml;
321
+ } else {
322
+ toNode = toElement(toNode);
323
+ }
324
+ }
325
+
326
+ var getNodeKey = options.getNodeKey || defaultGetNodeKey;
327
+ var onBeforeNodeAdded = options.onBeforeNodeAdded || noop;
328
+ var onNodeAdded = options.onNodeAdded || noop;
329
+ var onBeforeElUpdated = options.onBeforeElUpdated || noop;
330
+ var onElUpdated = options.onElUpdated || noop;
331
+ var onBeforeNodeDiscarded = options.onBeforeNodeDiscarded || noop;
332
+ var onNodeDiscarded = options.onNodeDiscarded || noop;
333
+ var onBeforeElChildrenUpdated = options.onBeforeElChildrenUpdated || noop;
334
+ var childrenOnly = options.childrenOnly === true;
335
+
336
+ // This object is used as a lookup to quickly find all keyed elements in the original DOM tree.
337
+ var fromNodesLookup = Object.create(null);
338
+ var keyedRemovalList = [];
339
+
340
+ function addKeyedRemoval(key) {
341
+ keyedRemovalList.push(key);
342
+ }
343
+
344
+ function walkDiscardedChildNodes(node, skipKeyedNodes) {
345
+ if (node.nodeType === ELEMENT_NODE) {
346
+ var curChild = node.firstChild;
347
+ while (curChild) {
348
+
349
+ var key = undefined;
350
+
351
+ if (skipKeyedNodes && (key = getNodeKey(curChild))) {
352
+ // If we are skipping keyed nodes then we add the key
353
+ // to a list so that it can be handled at the very end.
354
+ addKeyedRemoval(key);
355
+ } else {
356
+ // Only report the node as discarded if it is not keyed. We do this because
357
+ // at the end we loop through all keyed elements that were unmatched
358
+ // and then discard them in one final pass.
359
+ onNodeDiscarded(curChild);
360
+ if (curChild.firstChild) {
361
+ walkDiscardedChildNodes(curChild, skipKeyedNodes);
362
+ }
363
+ }
364
+
365
+ curChild = curChild.nextSibling;
366
+ }
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Removes a DOM node out of the original DOM
372
+ *
373
+ * @param {Node} node The node to remove
374
+ * @param {Node} parentNode The nodes parent
375
+ * @param {Boolean} skipKeyedNodes If true then elements with keys will be skipped and not discarded.
376
+ * @return {undefined}
377
+ */
378
+ function removeNode(node, parentNode, skipKeyedNodes) {
379
+ if (onBeforeNodeDiscarded(node) === false) {
380
+ return;
381
+ }
382
+
383
+ if (parentNode) {
384
+ parentNode.removeChild(node);
385
+ }
386
+
387
+ onNodeDiscarded(node);
388
+ walkDiscardedChildNodes(node, skipKeyedNodes);
389
+ }
390
+
391
+ // // TreeWalker implementation is no faster, but keeping this around in case this changes in the future
392
+ // function indexTree(root) {
393
+ // var treeWalker = document.createTreeWalker(
394
+ // root,
395
+ // NodeFilter.SHOW_ELEMENT);
396
+ //
397
+ // var el;
398
+ // while((el = treeWalker.nextNode())) {
399
+ // var key = getNodeKey(el);
400
+ // if (key) {
401
+ // fromNodesLookup[key] = el;
402
+ // }
403
+ // }
404
+ // }
405
+
406
+ // // NodeIterator implementation is no faster, but keeping this around in case this changes in the future
407
+ //
408
+ // function indexTree(node) {
409
+ // var nodeIterator = document.createNodeIterator(node, NodeFilter.SHOW_ELEMENT);
410
+ // var el;
411
+ // while((el = nodeIterator.nextNode())) {
412
+ // var key = getNodeKey(el);
413
+ // if (key) {
414
+ // fromNodesLookup[key] = el;
415
+ // }
416
+ // }
417
+ // }
418
+
419
+ function indexTree(node) {
420
+ if (node.nodeType === ELEMENT_NODE || node.nodeType === DOCUMENT_FRAGMENT_NODE$1) {
421
+ var curChild = node.firstChild;
422
+ while (curChild) {
423
+ var key = getNodeKey(curChild);
424
+ if (key) {
425
+ fromNodesLookup[key] = curChild;
426
+ }
427
+
428
+ // Walk recursively
429
+ indexTree(curChild);
430
+
431
+ curChild = curChild.nextSibling;
432
+ }
433
+ }
434
+ }
435
+
436
+ indexTree(fromNode);
437
+
438
+ function handleNodeAdded(el) {
439
+ onNodeAdded(el);
440
+
441
+ var curChild = el.firstChild;
442
+ while (curChild) {
443
+ var nextSibling = curChild.nextSibling;
444
+
445
+ var key = getNodeKey(curChild);
446
+ if (key) {
447
+ var unmatchedFromEl = fromNodesLookup[key];
448
+ // if we find a duplicate #id node in cache, replace `el` with cache value
449
+ // and morph it to the child node.
450
+ if (unmatchedFromEl && compareNodeNames(curChild, unmatchedFromEl)) {
451
+ curChild.parentNode.replaceChild(unmatchedFromEl, curChild);
452
+ morphEl(unmatchedFromEl, curChild);
453
+ } else {
454
+ handleNodeAdded(curChild);
455
+ }
456
+ } else {
457
+ // recursively call for curChild and it's children to see if we find something in
458
+ // fromNodesLookup
459
+ handleNodeAdded(curChild);
460
+ }
461
+
462
+ curChild = nextSibling;
463
+ }
464
+ }
465
+
466
+ function cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey) {
467
+ // We have processed all of the "to nodes". If curFromNodeChild is
468
+ // non-null then we still have some from nodes left over that need
469
+ // to be removed
470
+ while (curFromNodeChild) {
471
+ var fromNextSibling = curFromNodeChild.nextSibling;
472
+ if ((curFromNodeKey = getNodeKey(curFromNodeChild))) {
473
+ // Since the node is keyed it might be matched up later so we defer
474
+ // the actual removal to later
475
+ addKeyedRemoval(curFromNodeKey);
476
+ } else {
477
+ // NOTE: we skip nested keyed nodes from being removed since there is
478
+ // still a chance they will be matched up later
479
+ removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
480
+ }
481
+ curFromNodeChild = fromNextSibling;
482
+ }
483
+ }
484
+
485
+ function morphEl(fromEl, toEl, childrenOnly) {
486
+ var toElKey = getNodeKey(toEl);
487
+
488
+ if (toElKey) {
489
+ // If an element with an ID is being morphed then it will be in the final
490
+ // DOM so clear it out of the saved elements collection
491
+ delete fromNodesLookup[toElKey];
492
+ }
493
+
494
+ if (!childrenOnly) {
495
+ // optional
496
+ if (onBeforeElUpdated(fromEl, toEl) === false) {
497
+ return;
498
+ }
499
+
500
+ // update attributes on original DOM element first
501
+ morphAttrs(fromEl, toEl);
502
+ // optional
503
+ onElUpdated(fromEl);
504
+
505
+ if (onBeforeElChildrenUpdated(fromEl, toEl) === false) {
506
+ return;
507
+ }
508
+ }
509
+
510
+ if (fromEl.nodeName !== 'TEXTAREA') {
511
+ morphChildren(fromEl, toEl);
512
+ } else {
513
+ specialElHandlers.TEXTAREA(fromEl, toEl);
514
+ }
515
+ }
516
+
517
+ function morphChildren(fromEl, toEl) {
518
+ var curToNodeChild = toEl.firstChild;
519
+ var curFromNodeChild = fromEl.firstChild;
520
+ var curToNodeKey;
521
+ var curFromNodeKey;
522
+
523
+ var fromNextSibling;
524
+ var toNextSibling;
525
+ var matchingFromEl;
526
+
527
+ // walk the children
528
+ outer: while (curToNodeChild) {
529
+ toNextSibling = curToNodeChild.nextSibling;
530
+ curToNodeKey = getNodeKey(curToNodeChild);
531
+
532
+ // walk the fromNode children all the way through
533
+ while (curFromNodeChild) {
534
+ fromNextSibling = curFromNodeChild.nextSibling;
535
+
536
+ if (curToNodeChild.isSameNode && curToNodeChild.isSameNode(curFromNodeChild)) {
537
+ curToNodeChild = toNextSibling;
538
+ curFromNodeChild = fromNextSibling;
539
+ continue outer;
540
+ }
541
+
542
+ curFromNodeKey = getNodeKey(curFromNodeChild);
543
+
544
+ var curFromNodeType = curFromNodeChild.nodeType;
545
+
546
+ // this means if the curFromNodeChild doesnt have a match with the curToNodeChild
547
+ var isCompatible = undefined;
548
+
549
+ if (curFromNodeType === curToNodeChild.nodeType) {
550
+ if (curFromNodeType === ELEMENT_NODE) {
551
+ // Both nodes being compared are Element nodes
552
+
553
+ if (curToNodeKey) {
554
+ // The target node has a key so we want to match it up with the correct element
555
+ // in the original DOM tree
556
+ if (curToNodeKey !== curFromNodeKey) {
557
+ // The current element in the original DOM tree does not have a matching key so
558
+ // let's check our lookup to see if there is a matching element in the original
559
+ // DOM tree
560
+ if ((matchingFromEl = fromNodesLookup[curToNodeKey])) {
561
+ if (fromNextSibling === matchingFromEl) {
562
+ // Special case for single element removals. To avoid removing the original
563
+ // DOM node out of the tree (since that can break CSS transitions, etc.),
564
+ // we will instead discard the current node and wait until the next
565
+ // iteration to properly match up the keyed target element with its matching
566
+ // element in the original tree
567
+ isCompatible = false;
568
+ } else {
569
+ // We found a matching keyed element somewhere in the original DOM tree.
570
+ // Let's move the original DOM node into the current position and morph
571
+ // it.
572
+
573
+ // NOTE: We use insertBefore instead of replaceChild because we want to go through
574
+ // the `removeNode()` function for the node that is being discarded so that
575
+ // all lifecycle hooks are correctly invoked
576
+ fromEl.insertBefore(matchingFromEl, curFromNodeChild);
577
+
578
+ // fromNextSibling = curFromNodeChild.nextSibling;
579
+
580
+ if (curFromNodeKey) {
581
+ // Since the node is keyed it might be matched up later so we defer
582
+ // the actual removal to later
583
+ addKeyedRemoval(curFromNodeKey);
584
+ } else {
585
+ // NOTE: we skip nested keyed nodes from being removed since there is
586
+ // still a chance they will be matched up later
587
+ removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
588
+ }
589
+
590
+ curFromNodeChild = matchingFromEl;
591
+ }
592
+ } else {
593
+ // The nodes are not compatible since the "to" node has a key and there
594
+ // is no matching keyed node in the source tree
595
+ isCompatible = false;
596
+ }
597
+ }
598
+ } else if (curFromNodeKey) {
599
+ // The original has a key
600
+ isCompatible = false;
601
+ }
602
+
603
+ isCompatible = isCompatible !== false && compareNodeNames(curFromNodeChild, curToNodeChild);
604
+ if (isCompatible) {
605
+ // We found compatible DOM elements so transform
606
+ // the current "from" node to match the current
607
+ // target DOM node.
608
+ // MORPH
609
+ morphEl(curFromNodeChild, curToNodeChild);
610
+ }
611
+
612
+ } else if (curFromNodeType === TEXT_NODE || curFromNodeType == COMMENT_NODE) {
613
+ // Both nodes being compared are Text or Comment nodes
614
+ isCompatible = true;
615
+ // Simply update nodeValue on the original node to
616
+ // change the text value
617
+ if (curFromNodeChild.nodeValue !== curToNodeChild.nodeValue) {
618
+ curFromNodeChild.nodeValue = curToNodeChild.nodeValue;
619
+ }
620
+
621
+ }
622
+ }
623
+
624
+ if (isCompatible) {
625
+ // Advance both the "to" child and the "from" child since we found a match
626
+ // Nothing else to do as we already recursively called morphChildren above
627
+ curToNodeChild = toNextSibling;
628
+ curFromNodeChild = fromNextSibling;
629
+ continue outer;
630
+ }
631
+
632
+ // No compatible match so remove the old node from the DOM and continue trying to find a
633
+ // match in the original DOM. However, we only do this if the from node is not keyed
634
+ // since it is possible that a keyed node might match up with a node somewhere else in the
635
+ // target tree and we don't want to discard it just yet since it still might find a
636
+ // home in the final DOM tree. After everything is done we will remove any keyed nodes
637
+ // that didn't find a home
638
+ if (curFromNodeKey) {
639
+ // Since the node is keyed it might be matched up later so we defer
640
+ // the actual removal to later
641
+ addKeyedRemoval(curFromNodeKey);
642
+ } else {
643
+ // NOTE: we skip nested keyed nodes from being removed since there is
644
+ // still a chance they will be matched up later
645
+ removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
646
+ }
647
+
648
+ curFromNodeChild = fromNextSibling;
649
+ } // END: while(curFromNodeChild) {}
650
+
651
+ // If we got this far then we did not find a candidate match for
652
+ // our "to node" and we exhausted all of the children "from"
653
+ // nodes. Therefore, we will just append the current "to" node
654
+ // to the end
655
+ if (curToNodeKey && (matchingFromEl = fromNodesLookup[curToNodeKey]) && compareNodeNames(matchingFromEl, curToNodeChild)) {
656
+ fromEl.appendChild(matchingFromEl);
657
+ // MORPH
658
+ morphEl(matchingFromEl, curToNodeChild);
659
+ } else {
660
+ var onBeforeNodeAddedResult = onBeforeNodeAdded(curToNodeChild);
661
+ if (onBeforeNodeAddedResult !== false) {
662
+ if (onBeforeNodeAddedResult) {
663
+ curToNodeChild = onBeforeNodeAddedResult;
664
+ }
665
+
666
+ if (curToNodeChild.actualize) {
667
+ curToNodeChild = curToNodeChild.actualize(fromEl.ownerDocument || doc);
668
+ }
669
+ fromEl.appendChild(curToNodeChild);
670
+ handleNodeAdded(curToNodeChild);
671
+ }
672
+ }
673
+
674
+ curToNodeChild = toNextSibling;
675
+ curFromNodeChild = fromNextSibling;
676
+ }
677
+
678
+ cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey);
679
+
680
+ var specialElHandler = specialElHandlers[fromEl.nodeName];
681
+ if (specialElHandler) {
682
+ specialElHandler(fromEl, toEl);
683
+ }
684
+ } // END: morphChildren(...)
685
+
686
+ var morphedNode = fromNode;
687
+ var morphedNodeType = morphedNode.nodeType;
688
+ var toNodeType = toNode.nodeType;
689
+
690
+ if (!childrenOnly) {
691
+ // Handle the case where we are given two DOM nodes that are not
692
+ // compatible (e.g. <div> --> <span> or <div> --> TEXT)
693
+ if (morphedNodeType === ELEMENT_NODE) {
694
+ if (toNodeType === ELEMENT_NODE) {
695
+ if (!compareNodeNames(fromNode, toNode)) {
696
+ onNodeDiscarded(fromNode);
697
+ morphedNode = moveChildren(fromNode, createElementNS(toNode.nodeName, toNode.namespaceURI));
698
+ }
699
+ } else {
700
+ // Going from an element node to a text node
701
+ morphedNode = toNode;
702
+ }
703
+ } else if (morphedNodeType === TEXT_NODE || morphedNodeType === COMMENT_NODE) { // Text or comment node
704
+ if (toNodeType === morphedNodeType) {
705
+ if (morphedNode.nodeValue !== toNode.nodeValue) {
706
+ morphedNode.nodeValue = toNode.nodeValue;
707
+ }
708
+
709
+ return morphedNode;
710
+ } else {
711
+ // Text node to something else
712
+ morphedNode = toNode;
713
+ }
714
+ }
715
+ }
716
+
717
+ if (morphedNode === toNode) {
718
+ // The "to node" was not compatible with the "from node" so we had to
719
+ // toss out the "from node" and use the "to node"
720
+ onNodeDiscarded(fromNode);
721
+ } else {
722
+ if (toNode.isSameNode && toNode.isSameNode(morphedNode)) {
723
+ return;
724
+ }
725
+
726
+ morphEl(morphedNode, toNode, childrenOnly);
727
+
728
+ // We now need to loop over any keyed nodes that might need to be
729
+ // removed. We only do the removal if we know that the keyed node
730
+ // never found a match. When a keyed node is matched up we remove
731
+ // it out of fromNodesLookup and we use fromNodesLookup to determine
732
+ // if a keyed node has been matched up or not
733
+ if (keyedRemovalList) {
734
+ for (var i=0, len=keyedRemovalList.length; i<len; i++) {
735
+ var elToRemove = fromNodesLookup[keyedRemovalList[i]];
736
+ if (elToRemove) {
737
+ removeNode(elToRemove, elToRemove.parentNode, false);
738
+ }
739
+ }
740
+ }
741
+ }
742
+
743
+ if (!childrenOnly && morphedNode !== fromNode && fromNode.parentNode) {
744
+ if (morphedNode.actualize) {
745
+ morphedNode = morphedNode.actualize(fromNode.ownerDocument || doc);
746
+ }
747
+ // If we had to swap out the from node with a new node because the old
748
+ // node was not compatible with the target node then we need to
749
+ // replace the old DOM node in the original DOM tree. This is only
750
+ // possible if the original DOM node was part of a DOM tree which
751
+ // we know is the case if it has a parent node.
752
+ fromNode.parentNode.replaceChild(morphedNode, fromNode);
753
+ }
754
+
755
+ return morphedNode;
756
+ };
757
+ }
758
+
759
+ var morphdom = morphdomFactory(morphAttrs);
760
+
761
+ return morphdom;
762
+
763
+ }));