diff --git a/src/Elm/Kernel/VirtualDom.js b/src/Elm/Kernel/VirtualDom.js
index 022e63cf..61e9003e 100644
--- a/src/Elm/Kernel/VirtualDom.js
+++ b/src/Elm/Kernel/VirtualDom.js
@@ -2,7 +2,7 @@
import Basics exposing (identity)
import Elm.Kernel.Debug exposing (crash)
-import Elm.Kernel.Json exposing (equality, runHelp, unwrap, wrap)
+import Elm.Kernel.Json exposing (runHelp, unwrap, wrap)
import Elm.Kernel.List exposing (Cons, Nil)
import Elm.Kernel.Utils exposing (Tuple2)
import Elm.Kernel.Platform exposing (export)
@@ -14,9 +14,31 @@ import VirtualDom exposing (toHandlerInt)
+// Double underscore sequences are replaced with single letters or numbers.
+// Exactly which letter or number is used depends on the order the properties are first mentioned.
+// This preserves the letters and numbers from v1.0.3 for compatibility with tools that assume those exact values.
+// elm-explorations/test: https://github.com/elm-explorations/test/blob/d5eb84809de0f8bbf50303efd26889092c800609/src/Elm/Kernel/HtmlAsJson.js
+// elm-pages: https://github.com/dillonkearns/elm-pages/blob/fa1d0347016e20917b412de5c3657c2e6e095087/generator/src/build.js#L642
+// The list of names was extracted using the following commands:
+// # Switch to the reference commit:
+// git switch $old
+// # Find all relevant double underscore tokens.
+// grep --only --extended-regexp '_{2}[0-9a-z]\w+' src/Elm/Kernel/VirtualDom.js | awk '!visited[$0]++' >a.txt
+// # Switch to the current commit:
+// git switch $new
+// # Exclude the below line, then find all relevant double underscore tokens.
+// grep --invert-match void src/Elm/Kernel/VirtualDom.js | grep --only --extended-regexp '_{2}[0-9a-z]\w+' | awk '!visited[$0]++' >b.txt
+// # Keep only the double underscore tokens from the reference commit that still exist.
+// grep --fixed-strings --line-regexp --file=b.txt a.txt
+void { __2_TEXT: null, __text: null, __descendantsCount: null, __2_NODE: null, __tag: null, __facts: null, __kids: null, __namespace: null, __2_KEYED_NODE: null, __2_CUSTOM: null, __model: null, __render: null, __diff: null, __2_TAGGER: null, __tagger: null, __node: null, __2_THUNK: null, __refs: null, __thunk: null, __1_EVENT: null, __key: null, __value: null, __1_STYLE: null, __1_PROP: null, __1_ATTR: null, __1_ATTR_NS: null, __handler: null, __eventNode: null };
+
+
+
// HELPERS
+var _VirtualDom_everTranslated = false;
+
var _VirtualDom_divertHrefToApp;
var _VirtualDom_doc = typeof document !== 'undefined' ? document : {};
@@ -27,6 +49,71 @@ function _VirtualDom_appendChild(parent, child)
parent.appendChild(child);
}
+function _VirtualDom_insertBefore(parent, child, reference)
+{
+ if (!(child.parentNode === parent && child.nextSibling === reference))
+ {
+ parent.insertBefore(child, reference);
+ }
+}
+
+function _VirtualDom_insertAfter(parent, child, reference)
+{
+ if (!(child.parentNode === parent && child.previousSibling === reference))
+ {
+ parent.insertBefore(child, reference === null ? parent.firstChild : reference.nextSibling);
+ }
+}
+
+function _VirtualDom_moveBefore_(parent, child, reference)
+{
+ if (!(child.parentNode === parent && child.nextSibling === reference))
+ {
+ parent.moveBefore(child, reference);
+ }
+}
+
+function _VirtualDom_moveAfter_(parent, child, reference)
+{
+ if (!(child.parentNode === parent && child.previousSibling === reference))
+ {
+ parent.moveBefore(child, reference === null ? parent.firstChild : reference.nextSibling);
+ }
+}
+
+var _VirtualDom_supports_moveBefore = typeof Element !== 'undefined' && typeof Element.prototype.moveBefore === 'function';
+
+var _VirtualDom_moveBefore = _VirtualDom_supports_moveBefore ? _VirtualDom_moveBefore_ : _VirtualDom_insertBefore;
+
+var _VirtualDom_moveAfter = _VirtualDom_supports_moveBefore ? _VirtualDom_moveAfter_ : _VirtualDom_insertAfter;
+
+function _VirtualDom_remove(domNode)
+{
+ // An extension might have (re-)moved the element, so even if we have a
+ // reference to the `parentDomNode` that _should_ be the parent, we can’t
+ // just call `parentDomNode.removeChild(domNode)`. That throws an error if
+ // the node is not a child of `parentDomNode`.
+ var parentNode = domNode.parentNode;
+ if (parentNode)
+ {
+ parentNode.removeChild(domNode);
+ }
+}
+
+// A `tNode`, or “tree node”, is a tree structure that contains DOM nodes. The
+// children are keyed by index for regular nodes, and by key for keyed nodes.
+// This tree structure always matches the latest rendered virtual DOM tree,
+// while the real DOM tree might have been modified by browser extensions, page
+// translators and third party scripts. By using our own tree, we can guarantee
+// access to the DOM nodes we need, even if someone else has changed the page.
+function _VirtualDom_createTNode(domNode)
+{
+ return {
+ __domNode: domNode,
+ __children: Object.create(null)
+ };
+}
+
var _VirtualDom_init = F4(function(virtualNode, flagDecoder, debugMetadata, args)
{
// NOTE: this function needs __Platform_export available to work
@@ -39,7 +126,7 @@ var _VirtualDom_init = F4(function(virtualNode, flagDecoder, debugMetadata, args
//*/
node.parentNode.replaceChild(
- _VirtualDom_render(virtualNode, function() {}),
+ _VirtualDom_render(virtualNode, function() {}, _VirtualDom_createTNode(undefined)),
node
);
@@ -68,13 +155,10 @@ var _VirtualDom_nodeNS = F2(function(namespace, tag)
{
return F2(function(factList, kidList)
{
- for (var kids = [], descendantsCount = 0; kidList.b; kidList = kidList.b) // WHILE_CONS
+ for (var kids = []; kidList.b; kidList = kidList.b) // WHILE_CONS
{
- var kid = kidList.a;
- descendantsCount += (kid.__descendantsCount || 0);
- kids.push(kid);
+ kids.push(kidList.a);
}
- descendantsCount += kids.length;
return {
$: __2_NODE,
@@ -82,7 +166,10 @@ var _VirtualDom_nodeNS = F2(function(namespace, tag)
__facts: _VirtualDom_organizeFacts(factList),
__kids: kids,
__namespace: namespace,
- __descendantsCount: descendantsCount
+ // Unused, only exists for backwards compatibility with:
+ // https://github.com/elm-explorations/test/blob/9669a27d84fc29175364c7a60d5d700771a2801e/src/Test/Html/Internal/ElmHtml/InternalTypes.elm#L279
+ // https://github.com/dillonkearns/elm-pages/blob/fa1d0347016e20917b412de5c3657c2e6e095087/src/Test/Html/Internal/ElmHtml/InternalTypes.elm#L281
+ __descendantsCount: 0
};
});
});
@@ -99,21 +186,35 @@ var _VirtualDom_keyedNodeNS = F2(function(namespace, tag)
{
return F2(function(factList, kidList)
{
- for (var kids = [], descendantsCount = 0; kidList.b; kidList = kidList.b) // WHILE_CONS
+ for (var kids = [], kidsMap = Object.create(null); kidList.b; kidList = kidList.b) // WHILE_CONS
{
var kid = kidList.a;
- descendantsCount += (kid.b.__descendantsCount || 0);
+ var key = kid.a;
+ // Handle duplicate keys by adding a postfix.
+ while (key in kidsMap)
+ {
+ key += _VirtualDom_POSTFIX;
+ kid = __Utils_Tuple2(key, kid.b);
+ }
kids.push(kid);
+ kidsMap[key] = kid.b;
}
- descendantsCount += kids.length;
return {
$: __2_KEYED_NODE,
__tag: tag,
__facts: _VirtualDom_organizeFacts(factList),
+ // __kids holds the order and length of the kids.
__kids: kids,
+ // __kidsMap is a dict from key to node.
+ // Note when iterating JavaScript objects, numeric-looking keys come first.
+ // So we need both __kids and __kidsMap.
+ // Another reason is backwards compatibility with:
+ // https://github.com/elm-explorations/test/blob/d5eb84809de0f8bbf50303efd26889092c800609/src/Elm/Kernel/HtmlAsJson.js#L37
+ // https://github.com/dillonkearns/elm-pages/blob/fa1d0347016e20917b412de5c3657c2e6e095087/generator/src/build.js#L675
+ __kidsMap: kidsMap,
__namespace: namespace,
- __descendantsCount: descendantsCount
+ __descendantsCount: 0 // See _VirtualDom_nodeNS.
};
});
});
@@ -148,7 +249,7 @@ var _VirtualDom_map = F2(function(tagger, node)
$: __2_TAGGER,
__tagger: tagger,
__node: node,
- __descendantsCount: 1 + (node.__descendantsCount || 0)
+ __descendantsCount: 0 // See _VirtualDom_nodeNS.
};
});
@@ -388,9 +489,32 @@ var _VirtualDom_mapEventRecord = F2(function(func, record)
// ORGANIZE FACTS
+// This boolean is used to turn the `class` attribute into the `className` property only when needed for
+// backwards compatibility with elm-exploration/test (which only looks for `className` since `Html.Attributes.class` used to be implemented that way):
+// https://github.com/elm-explorations/test/blob/eef7f1aad0cc8c8b1434c678c757a1429fbcb9c7/src/Test/Html/Internal/ElmHtml/Query.elm#L265
+// Why not just keep `Html.Attributes.class` implemented as `className` then? Well, `Html.Attributes.class`
+// is better implemented as a `class` attribute rather than the `className` property because:
+// - In SVG, `className` is read only and throws an error if assigned. Setting the `class` attribute works.
+// - It’s easier to virtualize `class` since no special case mapping from `class` to `className` is needed.
+// - Properties are diffed against the actual DOM node. If a third-party script or browser extension add an
+// extra class on an element, that would be removed the next time Elm renders, even if nothing changed
+// about that element. Attributes are diffed against the previous virtual DOM, making it more likely that
+// extra added classes survive for some time.
+// - Properties are applied every render, even for lazy nodes, to make sure that for example `value` is up-to-date
+// (it might have been altered by the web page user by typing in some field). `Html.Attributes.class` is likely
+// one of the most used `Html.Attributes` functions in view code, and does not need to be applied every render.
+// So not doing that is a small performance win.
+var _VirtualDom_elmExplorationsTestBackwardsCompatibility = typeof _Test_runThunk === 'function';
+
+
function _VirtualDom_organizeFacts(factList)
{
- for (var facts = {}; factList.b; factList = factList.b) // WHILE_CONS
+ var facts = {};
+
+ // Mark all elements for virtualization of server rendered nodes – see `_VirtualDom_markerProperty`.
+ facts[_VirtualDom_markerProperty] = true;
+
+ for (; factList.b; factList = factList.b) // WHILE_CONS
{
var entry = factList.a;
@@ -409,7 +533,9 @@ function _VirtualDom_organizeFacts(factList)
var subFacts = facts[tag] || (facts[tag] = {});
(tag === 'a__1_ATTR' && key === 'class')
- ? _VirtualDom_addClass(subFacts, key, value)
+ ? _VirtualDom_elmExplorationsTestBackwardsCompatibility
+ ? _VirtualDom_addClass(facts, 'className', value)
+ : _VirtualDom_addClass(subFacts, key, value)
: subFacts[key] = value;
}
@@ -427,44 +553,32 @@ function _VirtualDom_addClass(object, key, newClass)
// RENDER
-function _VirtualDom_render(vNode, eventNode)
+function _VirtualDom_render(vNode, eventNode, tNode)
{
var tag = vNode.$;
if (tag === __2_THUNK)
{
- return _VirtualDom_render(vNode.__node || (vNode.__node = vNode.__thunk()), eventNode);
+ return _VirtualDom_render(vNode.__node || (vNode.__node = vNode.__thunk()), eventNode, tNode);
}
- if (tag === __2_TEXT)
+ if (tag === __2_TAGGER)
{
- return _VirtualDom_doc.createTextNode(vNode.__text);
+ return _VirtualDom_render(vNode.__node, function (msg, isSync) { return eventNode(vNode.__tagger(msg), isSync) }, tNode);
}
- if (tag === __2_TAGGER)
+ if (tag === __2_TEXT)
{
- var subNode = vNode.__node;
- var tagger = vNode.__tagger;
-
- while (subNode.$ === __2_TAGGER)
- {
- typeof tagger !== 'object'
- ? tagger = [tagger, subNode.__tagger]
- : tagger.push(subNode.__tagger);
-
- subNode = subNode.__node;
- }
-
- var subEventRoot = { __tagger: tagger, __parent: eventNode };
- var domNode = _VirtualDom_render(subNode, subEventRoot);
- domNode.elm_event_node_ref = subEventRoot;
+ var domNode = _VirtualDom_doc.createTextNode(vNode.__text);
+ tNode.__domNode = domNode;
return domNode;
}
if (tag === __2_CUSTOM)
{
var domNode = vNode.__render(vNode.__model);
- _VirtualDom_applyFacts(domNode, eventNode, vNode.__facts);
+ _VirtualDom_applyFacts(domNode, eventNode, {}, vNode.__facts);
+ tNode.__domNode = domNode;
return domNode;
}
@@ -479,40 +593,132 @@ function _VirtualDom_render(vNode, eventNode)
domNode.addEventListener('click', _VirtualDom_divertHrefToApp(domNode));
}
- _VirtualDom_applyFacts(domNode, eventNode, vNode.__facts);
+ _VirtualDom_applyFacts(domNode, eventNode, {}, vNode.__facts);
- for (var kids = vNode.__kids, i = 0; i < kids.length; i++)
+ if (tag === __2_NODE)
+ {
+ for (var kids = vNode.__kids, i = 0; i < kids.length; i++)
+ {
+ var childTNode = _VirtualDom_createTNode(undefined);
+ var childDomNode = _VirtualDom_render(kids[i], eventNode, childTNode);
+ tNode.__children[i] = childTNode;
+ _VirtualDom_appendChild(domNode, childDomNode);
+ }
+ }
+ else
{
- _VirtualDom_appendChild(domNode, _VirtualDom_render(tag === __2_NODE ? kids[i] : kids[i].b, eventNode));
+ for (var kids = vNode.__kids, i = 0; i < kids.length; i++)
+ {
+ var kid = kids[i];
+ var childTNode = _VirtualDom_createTNode(undefined);
+ var childDomNode = _VirtualDom_render(kid.b, eventNode, childTNode);
+ tNode.__children[kid.a] = childTNode;
+ _VirtualDom_appendChild(domNode, childDomNode);
+ }
}
+ tNode.__domNode = domNode;
+
return domNode;
}
+// Like `_VirtualDom_render`, but:
+// - Assumes that we have already gone through diffing.
+// - Only re-renders text nodes.
+function _VirtualDom_renderTranslated(vNode, eventNode, tNode)
+{
+ var tag = vNode.$;
+
+ if (tag === __2_THUNK)
+ {
+ return _VirtualDom_renderTranslated(vNode.__node, eventNode, tNode);
+ }
+
+ if (tag === __2_TAGGER)
+ {
+ return _VirtualDom_renderTranslated(vNode.__node, function (msg, isSync) { return eventNode(vNode.__tagger(msg), isSync) }, tNode);
+ }
+
+ if (tag === __2_TEXT)
+ {
+ var newNode = _VirtualDom_doc.createTextNode(vNode.__text);
+ tNode.__domNode = newNode;
+ return newNode;
+ }
+
+ return tNode.__domNode;
+}
+
// APPLY FACTS
-function _VirtualDom_applyFacts(domNode, eventNode, facts)
+function _VirtualDom_applyFacts(domNode, eventNode, prevFacts, facts)
{
- for (var key in facts)
+ // Since properties and attributes are sometimes linked, we need to remove old
+ // ones before setting new ones. Otherwise we might set the `id` attribute and
+ // then remove the `id` property, resulting in no id, for example.
+
+ if (prevFacts.a__1_STYLE !== undefined)
+ {
+ _VirtualDom_removeStyles(domNode, prevFacts.a__1_STYLE, facts.a__1_STYLE || {});
+ }
+
+ // `_VirtualDom_organizeFacts` puts properties directly on the `facts` object,
+ // instead of at `facts.a__1_PROP` which would have been more reasonable. So
+ // we pass the entire `facts` as the props, and `_VirtualDom_removeProps` needs
+ // to ignore `a__1_ATTR` etc.
+ // This results in that you can mess things up by setting properties called "a0" to "a4",
+ // but it’s not a big deal.
+ // We can’t fix this because of backwards compatibility with:
+ // https://github.com/elm-explorations/test/blob/9669a27d84fc29175364c7a60d5d700771a2801e/src/Test/Html/Internal/ElmHtml/InternalTypes.elm#L328
+ // https://github.com/dillonkearns/elm-pages/blob/fa1d0347016e20917b412de5c3657c2e6e095087/src/Test/Html/Internal/ElmHtml/InternalTypes.elm#L330
+ _VirtualDom_removeProps(domNode, prevFacts, facts);
+
+ if (prevFacts.a__1_ATTR !== undefined)
+ {
+ _VirtualDom_removeAttrs(domNode, prevFacts.a__1_ATTR, facts.a__1_ATTR || {});
+ }
+
+ if (prevFacts.a__1_ATTR_NS !== undefined)
+ {
+ _VirtualDom_removeAttrsNS(domNode, prevFacts.a__1_ATTR_NS, facts.a__1_ATTR_NS || {});
+ }
+
+ // Then, apply new facts.
+
+ if (facts.a__1_STYLE !== undefined)
+ {
+ _VirtualDom_applyStyles(domNode, prevFacts.a__1_STYLE || {}, facts.a__1_STYLE);
+ }
+
+ if (facts.a__1_ATTR !== undefined)
+ {
+ _VirtualDom_applyAttrs(domNode, prevFacts.a__1_ATTR || {}, facts.a__1_ATTR);
+ }
+
+ if (facts.a__1_ATTR_NS !== undefined)
{
- var value = facts[key];
+ _VirtualDom_applyAttrsNS(domNode, prevFacts.a__1_ATTR_NS || {}, facts.a__1_ATTR_NS);
+ }
+
+ // Apply properties _after_ attributes. This means that if you set the same
+ // thing both as a property and an attribute, the property wins. If the
+ // attribute had won, the property would “win” during the next render,
+ // since properties are diffed against the actual DOM node, while
+ // attributes are diffed against the previous virtual node. So it's better
+ // to let the property win right away.
+ // See the comment at the `_VirtualDom_removeProps` call earlier in this
+ // function for why we pass the entire `facts` object.
+ _VirtualDom_applyProps(domNode, facts);
- key === 'a__1_STYLE'
- ? _VirtualDom_applyStyles(domNode, value)
- :
- key === 'a__1_EVENT'
- ? _VirtualDom_applyEvents(domNode, eventNode, value)
- :
- key === 'a__1_ATTR'
- ? _VirtualDom_applyAttrs(domNode, value)
- :
- key === 'a__1_ATTR_NS'
- ? _VirtualDom_applyAttrsNS(domNode, value)
- :
- ((key !== 'value' && key !== 'checked') || domNode[key] !== value) && (domNode[key] = value);
+ // Finally, apply events. There is no separate phase for removing events.
+ // Attributes and properties can't interfere with events, so it's fine.
+
+ if (facts.a__1_EVENT !== undefined || prevFacts.a__1_EVENT !== undefined)
+ {
+ _VirtualDom_applyEvents(domNode, eventNode, facts.a__1_EVENT || {});
}
}
@@ -521,13 +727,112 @@ function _VirtualDom_applyFacts(domNode, eventNode, facts)
// APPLY STYLES
-function _VirtualDom_applyStyles(domNode, styles)
+function _VirtualDom_applyStyles(domNode, prevStyles, styles)
{
- var domNodeStyle = domNode.style;
-
for (var key in styles)
{
- domNodeStyle[key] = styles[key];
+ var value = styles[key];
+ if (value !== prevStyles[key])
+ {
+ // `.setProperty` must be used for `--custom-properties`.
+ // Standard properties never start with a dash.
+ // `.setProperty` requires for example 'border-radius' with a dash,
+ // while both `.style['border-radius']` and `.style['borderRadius']` work.
+ // Elm used to only use `.style`. In order to support existing code like
+ // `Html.Attributes.style 'borderRadius' '5px'` we default to `.style`
+ // and only use `.setProperty` if the property name starts with a dash.
+ if (key.charCodeAt(0) === 45)
+ {
+ domNode.style.setProperty(key, value);
+ }
+ else
+ {
+ domNode.style[key] = value;
+ }
+ }
+ }
+}
+
+
+function _VirtualDom_removeStyles(domNode, prevStyles, styles)
+{
+ for (var key in prevStyles)
+ {
+ if (!(key in styles))
+ {
+ // See `_VirtualDom_applyStyles`.
+ if (key.charCodeAt(0) === 45)
+ {
+ domNode.style.removeProperty(key);
+ }
+ else
+ {
+ domNode.style[key] = '';
+ }
+ }
+ }
+}
+
+
+
+// APPLY PROPS
+
+function _VirtualDom_applyProps(domNode, props)
+{
+ for (var key in props)
+ {
+ // See `_VirtualDom_applyFacts` and `_VirtualDom_markerProperty` for why we need to filter these.
+ if (key === 'a__1_EVENT' || key === 'a__1_STYLE' || key === 'a__1_ATTR' || key === 'a__1_ATTR_NS' || key === _VirtualDom_markerProperty)
+ {
+ continue;
+ }
+
+ var value = props[key];
+ // `value`, `checked`, `selected` and `selectedIndex` can all change via
+ // user interactions, so for those it’s important to compare to the
+ // actual DOM value. Because of that we compare against the actual DOM
+ // node, rather than `prevProps`. Note that many properties are
+ // normalized (to certain values, or to a full URL, for example), so if
+ // you use properties they might be set on every render if you don't
+ // supply the normalized form. `Html.Attributes` avoids this by
+ // primarily using attributes.
+ if (value !== domNode[key])
+ {
+ domNode[key] = value;
+ }
+ }
+}
+
+
+function _VirtualDom_removeProps(domNode, prevProps, props)
+{
+ for (var key in prevProps)
+ {
+ // See `_VirtualDom_applyFacts` and `_VirtualDom_markerProperty` for why we need to filter these.
+ if (key === 'a__1_EVENT' || key === 'a__1_STYLE' || key === 'a__1_ATTR' || key === 'a__1_ATTR_NS' || key === _VirtualDom_markerProperty)
+ {
+ continue;
+ }
+
+ if (!(key in props))
+ {
+ var value = prevProps[key];
+ switch (typeof value)
+ {
+ // Most string properties default to the empty string.
+ case 'string':
+ domNode[key] = '';
+ break;
+ // Most boolean properties default to false.
+ case 'boolean':
+ domNode[key] = false;
+ break;
+ // For other types it's unclear what to do.
+ }
+ // Standard properties cannot be deleted, but it is not an error trying.
+ // Non-standard properties can be deleted.
+ delete domNode[key];
+ }
}
}
@@ -536,14 +841,27 @@ function _VirtualDom_applyStyles(domNode, styles)
// APPLY ATTRS
-function _VirtualDom_applyAttrs(domNode, attrs)
+function _VirtualDom_applyAttrs(domNode, prevAttrs, attrs)
{
for (var key in attrs)
{
var value = attrs[key];
- typeof value !== 'undefined'
- ? domNode.setAttribute(key, value)
- : domNode.removeAttribute(key);
+ if (value !== prevAttrs[key])
+ {
+ domNode.setAttribute(key, value);
+ }
+ }
+}
+
+
+function _VirtualDom_removeAttrs(domNode, prevAttrs, attrs)
+{
+ for (var key in prevAttrs)
+ {
+ if (!(key in attrs))
+ {
+ domNode.removeAttribute(key);
+ }
}
}
@@ -552,17 +870,39 @@ function _VirtualDom_applyAttrs(domNode, attrs)
// APPLY NAMESPACED ATTRS
-function _VirtualDom_applyAttrsNS(domNode, nsAttrs)
+function _VirtualDom_applyAttrsNS(domNode, prevNsAttrs, nsAttrs)
{
for (var key in nsAttrs)
{
var pair = nsAttrs[key];
var namespace = pair.__namespace;
var value = pair.__value;
+ var previous = prevNsAttrs[key];
+ if (!previous)
+ {
+ domNode.setAttributeNS(namespace, key, value);
+ }
+ else if (previous.__namespace !== namespace)
+ {
+ domNode.removeAttributeNS(previous.__namespace, key);
+ domNode.setAttributeNS(namespace, key, value);
+ }
+ else if (previous.__value !== value)
+ {
+ domNode.setAttributeNS(namespace, key, value);
+ }
+ }
+}
+
- typeof value !== 'undefined'
- ? domNode.setAttributeNS(namespace, key, value)
- : domNode.removeAttributeNS(namespace, key);
+function _VirtualDom_removeAttrsNS(domNode, prevNsAttrs, nsAttrs)
+{
+ for (var key in prevNsAttrs)
+ {
+ if (!(key in nsAttrs))
+ {
+ domNode.removeAttributeNS(prevNsAttrs[key].__namespace, key);
+ }
}
}
@@ -583,7 +923,7 @@ function _VirtualDom_applyEvents(domNode, eventNode, events)
if (!newHandler)
{
domNode.removeEventListener(key, oldCallback);
- allCallbacks[key] = undefined;
+ delete allCallbacks[key];
continue;
}
@@ -593,6 +933,7 @@ function _VirtualDom_applyEvents(domNode, eventNode, events)
if (oldHandler.$ === newHandler.$)
{
oldCallback.__handler = newHandler;
+ oldCallback.__eventNode = eventNode;
continue;
}
domNode.removeEventListener(key, oldCallback);
@@ -605,6 +946,29 @@ function _VirtualDom_applyEvents(domNode, eventNode, events)
);
allCallbacks[key] = oldCallback;
}
+
+ for (key in allCallbacks)
+ {
+ if (!(key in events))
+ {
+ domNode.removeEventListener(key, allCallbacks[key]);
+ delete allCallbacks[key];
+ }
+ }
+}
+
+function _VirtualDom_lazyUpdateEvents(domNode, eventNode)
+{
+ var allCallbacks = domNode.elmFs;
+
+ if (allCallbacks)
+ {
+ for (var key in allCallbacks)
+ {
+ var oldCallback = allCallbacks[key];
+ oldCallback.__eventNode = eventNode;
+ }
+ }
}
@@ -627,11 +991,12 @@ catch(e) {}
// EVENT HANDLERS
-function _VirtualDom_makeCallback(eventNode, initialHandler)
+function _VirtualDom_makeCallback(initialEventNode, initialHandler)
{
function callback(event)
{
var handler = callback.__handler;
+ var eventNode = callback.__eventNode;
var result = __Json_runHelp(handler.a, event);
if (!__Result_isOk(result))
@@ -654,102 +1019,57 @@ function _VirtualDom_makeCallback(eventNode, initialHandler)
(tag == 2 ? value.b : tag == 3 && value.__$preventDefault) && event.preventDefault(),
eventNode
);
- var tagger;
- var i;
- while (tagger = currentEventNode.__tagger)
- {
- if (typeof tagger == 'function')
- {
- message = tagger(message);
- }
- else
- {
- for (var i = tagger.length; i--; )
- {
- message = tagger[i](message);
- }
- }
- currentEventNode = currentEventNode.__parent;
- }
currentEventNode(message, stopPropagation); // stopPropagation implies isSync
}
callback.__handler = initialHandler;
+ callback.__eventNode = initialEventNode;
return callback;
}
-function _VirtualDom_equalEvents(x, y)
-{
- return x.$ == y.$ && __Json_equality(x.a, y.a);
-}
-
// DIFF
-// TODO: Should we do patches like in iOS?
-//
-// type Patch
-// = At Int Patch
-// | Batch (List Patch)
-// | Change ...
-//
-// How could it not be better?
-//
-function _VirtualDom_diff(x, y)
-{
- var patches = [];
- _VirtualDom_diffHelp(x, y, patches, 0);
- return patches;
-}
-
-
-function _VirtualDom_pushPatch(patches, type, index, data)
+function _VirtualDom_diff(_x, y)
{
- var patch = {
- $: type,
- __index: index,
- __data: data,
- __domNode: undefined,
- __eventNode: undefined
- };
- patches.push(patch);
- return patch;
+ // Hack to provide the new virtual dom node to `_VirtualDom_applyPatches` without
+ // making breaking changes to elm/browser.
+ return y;
}
-
-function _VirtualDom_diffHelp(x, y, patches, index)
+function _VirtualDom_diffHelp(x, y, eventNode, tNode)
{
if (x === y)
{
- return;
+ return {
+ __domNode: _VirtualDom_quickVisit(x, y, eventNode, tNode),
+ __translated: false,
+ __reinsert: false
+ };
}
- var xType = x.$;
- var yType = y.$;
+ // Remember: When virtualizing already existing DOM, we can’t know
+ // where `map` and `lazy` nodes should be, and which ones are `Keyed`.
+ // So it’s important to not redraw fully when just the new virtual dom node
+ // is a `map` or `lazy` or `Keyed`, to avoid unnecessary DOM changes on startup.
- // Bail if you run into different types of nodes. Implies that the
- // structure has changed significantly and it's not worth a diff.
- if (xType !== yType)
+ while (x.$ === __2_TAGGER)
{
- if (xType === __2_NODE && yType === __2_KEYED_NODE)
- {
- y = _VirtualDom_dekey(y);
- yType = __2_NODE;
- }
- else
- {
- _VirtualDom_pushPatch(patches, __3_REDRAW, index, y);
- return;
- }
+ x = x.__node;
}
- // Now we know that both nodes are the same $.
- switch (yType)
+ if (y.$ === __2_TAGGER)
{
- case __2_THUNK:
+ return _VirtualDom_diffHelp(x, y.__node, function (msg, isSync) { return eventNode(y.__tagger(msg), isSync) }, tNode);
+ }
+
+ if (x.$ === __2_THUNK)
+ {
+ if (y.$ === __2_THUNK)
+ {
var xRefs = x.__refs;
var yRefs = y.__refs;
var i = xRefs.length;
@@ -761,194 +1081,251 @@ function _VirtualDom_diffHelp(x, y, patches, index)
if (same)
{
y.__node = x.__node;
- return;
+ // We still need to visit every node inside the lazy node, to
+ // make sure that the event listeners get the current
+ // `eventNode`, and to increase and reset counters. This is
+ // cheaper than calling `view`, diffing and rendering at least.
+ return {
+ __domNode: _VirtualDom_quickVisit(x, y, eventNode, tNode),
+ __translated: false,
+ __reinsert: false
+ };
}
y.__node = y.__thunk();
- var subPatches = [];
- _VirtualDom_diffHelp(x.__node, y.__node, subPatches, 0);
- subPatches.length > 0 && _VirtualDom_pushPatch(patches, __3_THUNK, index, subPatches);
- return;
-
- case __2_TAGGER:
- // gather nested taggers
- var xTaggers = x.__tagger;
- var yTaggers = y.__tagger;
- var nesting = false;
-
- var xSubNode = x.__node;
- while (xSubNode.$ === __2_TAGGER)
- {
- nesting = true;
+ return _VirtualDom_diffHelp(x.__node, y.__node, eventNode, tNode);
+ }
+ else
+ {
+ return _VirtualDom_diffHelp(x.__node, y, eventNode, tNode);
+ }
+ }
- typeof xTaggers !== 'object'
- ? xTaggers = [xTaggers, xSubNode.__tagger]
- : xTaggers.push(xSubNode.__tagger);
+ if (y.$ === __2_THUNK)
+ {
+ return _VirtualDom_diffHelp(x, y.__node || (y.__node = y.__thunk()), eventNode, tNode);
+ }
- xSubNode = xSubNode.__node;
- }
+ var domNode = tNode.__domNode;
- var ySubNode = y.__node;
- while (ySubNode.$ === __2_TAGGER)
- {
- nesting = true;
+ var xType = x.$;
+ var yType = y.$;
- typeof yTaggers !== 'object'
- ? yTaggers = [yTaggers, ySubNode.__tagger]
- : yTaggers.push(ySubNode.__tagger);
-
- ySubNode = ySubNode.__node;
- }
-
- // Just bail if different numbers of taggers. This implies the
- // structure of the virtual DOM has changed.
- if (nesting && xTaggers.length !== yTaggers.length)
- {
- _VirtualDom_pushPatch(patches, __3_REDRAW, index, y);
- return;
- }
-
- // check if taggers are "the same"
- if (nesting ? !_VirtualDom_pairwiseRefEqual(xTaggers, yTaggers) : xTaggers !== yTaggers)
- {
- _VirtualDom_pushPatch(patches, __3_TAGGER, index, yTaggers);
- }
-
- // diff everything below the taggers
- _VirtualDom_diffHelp(xSubNode, ySubNode, patches, index + 1);
- return;
+ // Bail if you run into different types of nodes. Implies that the
+ // structure has changed significantly and it's not worth a diff.
+ if (xType !== yType)
+ {
+ if (xType === __2_NODE && yType === __2_KEYED_NODE)
+ {
+ x = _VirtualDom_upkey(x, y, tNode);
+ xType = __2_KEYED_NODE;
+ }
+ else if (xType === __2_KEYED_NODE && yType === __2_NODE)
+ {
+ x = _VirtualDom_dekey(x, tNode);
+ xType = __2_NODE;
+ }
+ else
+ {
+ return _VirtualDom_applyPatchRedraw(x, y, eventNode, tNode);
+ }
+ }
+ // Now we know that both nodes are the same $.
+ switch (yType)
+ {
case __2_TEXT:
if (x.__text !== y.__text)
{
- _VirtualDom_pushPatch(patches, __3_TEXT, index, y.__text);
+ // Text replaced or changed by translation plugins.
+ if (!domNode.parentNode || domNode.data !== x.__text)
+ {
+ return {
+ __domNode: domNode,
+ __translated: true,
+ __reinsert: false
+ };
+ }
+ // Google Translate has a race condition-style bug where if you update the text
+ // of a text node while it is fetching a translation for it, you’ll end up with
+ // that out-of-date translation. So if we’ve ever detected a translation, it’s
+ // no longer safe to update text nodes. Instead, we must replace them with new ones.
+ // That’s slower, so we only switch to this method if needed.
+ // See: https://issues.chromium.org/issues/393698470
+ if (_VirtualDom_everTranslated)
+ {
+ var newNode = _VirtualDom_doc.createTextNode(y.__text);
+ tNode.__domNode = newNode;
+ domNode.parentNode.replaceChild(newNode, domNode);
+ domNode = newNode;
+ }
+ else
+ {
+ domNode.data = y.__text;
+ }
}
- return;
+ return {
+ __domNode: domNode,
+ __translated: false,
+ __reinsert: false
+ };
case __2_NODE:
- _VirtualDom_diffNodes(x, y, patches, index, _VirtualDom_diffKids);
- return;
+ return _VirtualDom_diffNodes(domNode, x, y, eventNode, tNode, _VirtualDom_diffKids);
case __2_KEYED_NODE:
- _VirtualDom_diffNodes(x, y, patches, index, _VirtualDom_diffKeyedKids);
- return;
+ return _VirtualDom_diffNodes(domNode, x, y, eventNode, tNode, _VirtualDom_diffKeyedKids);
case __2_CUSTOM:
if (x.__render !== y.__render)
{
- _VirtualDom_pushPatch(patches, __3_REDRAW, index, y);
- return;
+ return _VirtualDom_applyPatchRedraw(x, y, eventNode, tNode);
}
- var factsDiff = _VirtualDom_diffFacts(x.__facts, y.__facts);
- factsDiff && _VirtualDom_pushPatch(patches, __3_FACTS, index, factsDiff);
+ _VirtualDom_applyFacts(domNode, eventNode, x.__facts, y.__facts);
var patch = y.__diff(x.__model, y.__model);
- patch && _VirtualDom_pushPatch(patches, __3_CUSTOM, index, patch);
+ patch && patch(domNode);
- return;
+ return {
+ __domNode: domNode,
+ __translated: false,
+ __reinsert: false
+ };
}
}
-// assumes the incoming arrays are the same length
-function _VirtualDom_pairwiseRefEqual(as, bs)
+// When we know that a node does not need updating, just quickly visit its children to:
+// - Make sure that properties match the virtual node – they can be mutated by user actions, such as typing into an input.
+// `Html.Attributes` primarily uses attributes (not properties), so this shouldn’t take much time.
+// - Update event listeners’ reference to the current `eventNode`.
+function _VirtualDom_quickVisit(x, y, eventNode, tNode)
{
- for (var i = 0; i < as.length; i++)
+ switch (y.$)
{
- if (as[i] !== bs[i])
- {
- return false;
- }
+ case __2_TAGGER:
+ return _VirtualDom_quickVisit(x.__node, y.__node, function (msg, isSync) { return eventNode(y.__tagger(msg), isSync) }, tNode);
+
+ case __2_THUNK:
+ return _VirtualDom_quickVisit(x.__node, y.__node, eventNode, tNode);
}
- return true;
+ var domNode = tNode.__domNode;
+
+ switch (y.$)
+ {
+ case __2_TEXT:
+ return domNode;
+
+ case __2_NODE:
+ _VirtualDom_applyProps(domNode, y.__facts);
+ _VirtualDom_lazyUpdateEvents(domNode, eventNode);
+ for (var xKids = x.__kids, yKids = y.__kids, i = 0; i < yKids.length; i++)
+ {
+ _VirtualDom_quickVisit(xKids[i], yKids[i], eventNode, tNode.__children[i]);
+ }
+ return domNode;
+
+ case __2_KEYED_NODE:
+ _VirtualDom_applyProps(domNode, y.__facts);
+ _VirtualDom_lazyUpdateEvents(domNode, eventNode);
+ for (var xKids = x.__kids, yKids = y.__kids, i = 0; i < yKids.length; i++)
+ {
+ var xKid = xKids[i];
+ var yKid = yKids[i];
+ _VirtualDom_quickVisit(xKid.b, yKid.b, eventNode, tNode.__children[yKid.a]);
+ }
+ return domNode;
+
+ case __2_CUSTOM:
+ _VirtualDom_applyProps(domNode, y.__facts);
+ _VirtualDom_lazyUpdateEvents(domNode, eventNode);
+ return domNode;
+ }
}
-function _VirtualDom_diffNodes(x, y, patches, index, diffKids)
+function _VirtualDom_diffNodes(domNode, x, y, eventNode, tNode, diffKids)
{
// Bail if obvious indicators have changed. Implies more serious
// structural changes such that it's not worth it to diff.
if (x.__tag !== y.__tag || x.__namespace !== y.__namespace)
{
- _VirtualDom_pushPatch(patches, __3_REDRAW, index, y);
- return;
+ return _VirtualDom_applyPatchRedraw(x, y, eventNode, tNode);
}
- var factsDiff = _VirtualDom_diffFacts(x.__facts, y.__facts);
- factsDiff && _VirtualDom_pushPatch(patches, __3_FACTS, index, factsDiff);
-
- diffKids(x, y, patches, index);
-}
-
-
-
-// DIFF FACTS
+ _VirtualDom_applyFacts(domNode, eventNode, x.__facts, y.__facts);
+ var translated = diffKids(domNode, x, y, eventNode, tNode);
-// TODO Instead of creating a new diff object, it's possible to just test if
-// there *is* a diff. During the actual patch, do the diff again and make the
-// modifications directly. This way, there's no new allocations. Worth it?
-function _VirtualDom_diffFacts(x, y, category)
-{
- var diff;
-
- // look for changes and removals
- for (var xKey in x)
+ // If at least one kid was detected to have been translated (by Google Translate for example),
+ // we need to go through all kids and actual DOM node children once more. If a text node
+ // has been replaced by another with translated text, we don’t know _which_ text node it has
+ // been replace by. We have to rerender _all_ text inside the element. This has the side benefit
+ // of increasing the likelihood of getting a well-formed sentence after the translator re-translates
+ // the text. Since different languages have different word order, it’s the best to translate
+ // whole sentences at the minimum. It’s difficult to heuristically find a sentence or paragraph
+ // though. “All the text directly inside this element” is the best we’ve got so far.
+ if (translated)
{
- if (xKey === 'a__1_STYLE' || xKey === 'a__1_EVENT' || xKey === 'a__1_ATTR' || xKey === 'a__1_ATTR_NS')
- {
- var subDiff = _VirtualDom_diffFacts(x[xKey], y[xKey] || {}, xKey);
- if (subDiff)
- {
- diff = diff || {};
- diff[xKey] = subDiff;
- }
- continue;
- }
+ _VirtualDom_everTranslated = true;
- // remove if not in the new facts
- if (!(xKey in y))
+ for (var current = null, kids = y.__kids, i = kids.length - 1, j = domNode.childNodes.length - 1; i >= 0; i--)
{
- diff = diff || {};
- diff[xKey] =
- !category
- ? (typeof x[xKey] === 'string' ? '' : null)
- :
- (category === 'a__1_STYLE')
- ? ''
- :
- (category === 'a__1_EVENT' || category === 'a__1_ATTR')
- ? undefined
- :
- { __namespace: x[xKey].__namespace, __value: undefined };
-
- continue;
- }
+ var kid = kids[i];
+ var vNode = y.$ === __2_KEYED_NODE ? kid.b : kid;
- var xValue = x[xKey];
- var yValue = y[xKey];
+ // `child` is going to be one of:
+ // - For text nodes: A new text node that isn’t inserted into the DOM.
+ // - For other nodes: The already existing DOM node. An extension
+ // might have removed it, though, or moved it to another parent.
+ var child = _VirtualDom_renderTranslated(vNode, eventNode, tNode.__children[y.$ === __2_KEYED_NODE ? kid.a : i]);
- // reference equal, so don't worry about it
- if (xValue === yValue && xKey !== 'value' && xKey !== 'checked'
- || category === 'a__1_EVENT' && _VirtualDom_equalEvents(xValue, yValue))
- {
- continue;
+ if (child.parentNode === domNode)
+ {
+ // Go through the actual children of `domNode` until we hit `child`,
+ // which we just checked for sure is a child of `domNode`. We know
+ // that all “our” kids are in the correct order.
+ for (; j >= 0; j--)
+ {
+ current = domNode.childNodes[j];
+ if (current === child)
+ {
+ j--;
+ break;
+ }
+ // Any element we come across until we find `child` must be created by others,
+ // or be text nodes created by us but abandoned in `_VirtualDom_renderTranslated`.
+ // Remove all text nodes, and all font tags (most likely created by Google Translate).
+ if (current.nodeType === 3 || current.localName === 'font')
+ {
+ domNode.removeChild(current);
+ }
+ }
+ }
+ else
+ {
+ // Most likely, we are inserting a new text node here.
+ // It could also be an element (re-)moved by an extension.
+ _VirtualDom_insertBefore(domNode, child, current);
+ current = child;
+ }
}
- diff = diff || {};
- diff[xKey] = yValue;
- }
-
- // add new stuff
- for (var yKey in y)
- {
- if (!(yKey in x))
+ // If there are more elements before our first kid, go through them as well like above.
+ for (; j >= 0; j--)
{
- diff = diff || {};
- diff[yKey] = y[yKey];
+ current = domNode.childNodes[j];
+ if (current.nodeType === 3 || current.localName === 'font')
+ {
+ domNode.removeChild(current);
+ }
}
}
- return diff;
+ return {
+ __domNode: domNode,
+ __translated: false,
+ __reinsert: false
+ };
}
@@ -956,7 +1333,7 @@ function _VirtualDom_diffFacts(x, y, category)
// DIFF KIDS
-function _VirtualDom_diffKids(xParent, yParent, patches, index)
+function _VirtualDom_diffKids(parentDomNode, xParent, yParent, eventNode, tNode)
{
var xKids = xParent.__kids;
var yKids = yParent.__kids;
@@ -964,570 +1341,485 @@ function _VirtualDom_diffKids(xParent, yParent, patches, index)
var xLen = xKids.length;
var yLen = yKids.length;
- // FIGURE OUT IF THERE ARE INSERTS OR REMOVALS
-
- if (xLen > yLen)
- {
- _VirtualDom_pushPatch(patches, __3_REMOVE_LAST, index, {
- __length: yLen,
- __diff: xLen - yLen
- });
- }
- else if (xLen < yLen)
- {
- _VirtualDom_pushPatch(patches, __3_APPEND, index, {
- __length: xLen,
- __kids: yKids
- });
- }
+ var translated = false;
+ var previousSibling = null;
- // PAIRWISE DIFF EVERYTHING ELSE
+ // PAIRWISE DIFF COMMON KIDS
for (var minLen = xLen < yLen ? xLen : yLen, i = 0; i < minLen; i++)
{
- var xKid = xKids[i];
- _VirtualDom_diffHelp(xKid, yKids[i], patches, ++index);
- index += xKid.__descendantsCount || 0;
- }
-}
-
-
-
-// KEYED DIFF
-
-
-function _VirtualDom_diffKeyedKids(xParent, yParent, patches, rootIndex)
-{
- var localPatches = [];
-
- var changes = {}; // Dict String Entry
- var inserts = []; // Array { index : Int, entry : Entry }
- // type Entry = { tag : String, vnode : VNode, index : Int, data : _ }
-
- var xKids = xParent.__kids;
- var yKids = yParent.__kids;
- var xLen = xKids.length;
- var yLen = yKids.length;
- var xIndex = 0;
- var yIndex = 0;
-
- var index = rootIndex;
-
- while (xIndex < xLen && yIndex < yLen)
- {
- var x = xKids[xIndex];
- var y = yKids[yIndex];
-
- var xKey = x.a;
- var yKey = y.a;
- var xNode = x.b;
- var yNode = y.b;
-
- var newMatch = undefined;
- var oldMatch = undefined;
+ var diffReturn = _VirtualDom_diffHelp(xKids[i], yKids[i], eventNode, tNode.__children[i]);
+ var domNode = diffReturn.__domNode;
- // check if keys match
-
- if (xKey === yKey)
+ if (diffReturn.__translated)
{
- index++;
- _VirtualDom_diffHelp(xNode, yNode, localPatches, index);
- index += xNode.__descendantsCount || 0;
-
- xIndex++;
- yIndex++;
- continue;
+ translated = true;
}
- // look ahead 1 to detect insertions and removals.
-
- var xNext = xKids[xIndex + 1];
- var yNext = yKids[yIndex + 1];
-
- if (xNext)
+ if (diffReturn.__reinsert)
{
- var xNextKey = xNext.a;
- var xNextNode = xNext.b;
- oldMatch = yKey === xNextKey;
+ _VirtualDom_insertAfter(parentDomNode, domNode, previousSibling);
+ previousSibling = domNode;
}
-
- if (yNext)
+ // An extension might have removed an element we have rendered before,
+ // or moved it to another parent. In such cases, `parentDomNode.insertBefore(x, domNode)`
+ // would throw errors. Keep the previous reference element in those cases – that should still
+ // result in the correct element order, just with some element missing.
+ else if (domNode.parentNode === parentDomNode)
{
- var yNextKey = yNext.a;
- var yNextNode = yNext.b;
- newMatch = xKey === yNextKey;
- }
-
-
- // swap x and y
- if (newMatch && oldMatch)
- {
- index++;
- _VirtualDom_diffHelp(xNode, yNextNode, localPatches, index);
- _VirtualDom_insertNode(changes, localPatches, xKey, yNode, yIndex, inserts);
- index += xNode.__descendantsCount || 0;
-
- index++;
- _VirtualDom_removeNode(changes, localPatches, xKey, xNextNode, index);
- index += xNextNode.__descendantsCount || 0;
-
- xIndex += 2;
- yIndex += 2;
- continue;
- }
-
- // insert y
- if (newMatch)
- {
- index++;
- _VirtualDom_insertNode(changes, localPatches, yKey, yNode, yIndex, inserts);
- _VirtualDom_diffHelp(xNode, yNextNode, localPatches, index);
- index += xNode.__descendantsCount || 0;
-
- xIndex += 1;
- yIndex += 2;
- continue;
- }
-
- // remove x
- if (oldMatch)
- {
- index++;
- _VirtualDom_removeNode(changes, localPatches, xKey, xNode, index);
- index += xNode.__descendantsCount || 0;
-
- index++;
- _VirtualDom_diffHelp(xNextNode, yNode, localPatches, index);
- index += xNextNode.__descendantsCount || 0;
-
- xIndex += 2;
- yIndex += 1;
- continue;
- }
-
- // remove x, insert y
- if (xNext && xNextKey === yNextKey)
- {
- index++;
- _VirtualDom_removeNode(changes, localPatches, xKey, xNode, index);
- _VirtualDom_insertNode(changes, localPatches, yKey, yNode, yIndex, inserts);
- index += xNode.__descendantsCount || 0;
-
- index++;
- _VirtualDom_diffHelp(xNextNode, yNextNode, localPatches, index);
- index += xNextNode.__descendantsCount || 0;
-
- xIndex += 2;
- yIndex += 2;
- continue;
+ previousSibling = domNode;
}
-
- break;
}
- // eat up any remaining nodes with removeNode and insertNode
+ // FIGURE OUT IF THERE ARE INSERTS OR REMOVALS
- while (xIndex < xLen)
+ if (xLen > yLen)
{
- index++;
- var x = xKids[xIndex];
- var xNode = x.b;
- _VirtualDom_removeNode(changes, localPatches, x.a, xNode, index);
- index += xNode.__descendantsCount || 0;
- xIndex++;
+ for (var i = yLen; i < xLen; i++)
+ {
+ _VirtualDom_remove(tNode.__children[i].__domNode);
+ delete tNode.__children[i];
+ }
}
-
- while (yIndex < yLen)
+ else if (xLen < yLen)
{
- var endInserts = endInserts || [];
- var y = yKids[yIndex];
- _VirtualDom_insertNode(changes, localPatches, y.a, y.b, undefined, endInserts);
- yIndex++;
+ for (var i = xLen; i < yLen; i++)
+ {
+ var y = yKids[i];
+ var childTNode = _VirtualDom_createTNode(undefined);
+ var domNode = _VirtualDom_render(y, eventNode, childTNode);
+ tNode.__children[i] = childTNode;
+ _VirtualDom_appendChild(parentDomNode, domNode);
+ }
}
- if (localPatches.length > 0 || inserts.length > 0 || endInserts)
- {
- _VirtualDom_pushPatch(patches, __3_REORDER, rootIndex, {
- __patches: localPatches,
- __inserts: inserts,
- __endInserts: endInserts
- });
- }
+ return translated;
}
-// CHANGES FROM KEYED DIFF
-
-
-var _VirtualDom_POSTFIX = '_elmW6BL';
-
-
-function _VirtualDom_insertNode(changes, localPatches, key, vnode, yIndex, inserts)
-{
- var entry = changes[key];
-
- // never seen this key before
- if (!entry)
- {
- entry = {
- __tag: __5_INSERT,
- __vnode: vnode,
- __index: yIndex,
- __data: undefined
- };
-
- inserts.push({ __index: yIndex, __entry: entry });
- changes[key] = entry;
-
- return;
- }
-
- // this key was removed earlier, a match!
- if (entry.__tag === __5_REMOVE)
- {
- inserts.push({ __index: yIndex, __entry: entry });
-
- entry.__tag = __5_MOVE;
- var subPatches = [];
- _VirtualDom_diffHelp(entry.__vnode, vnode, subPatches, entry.__index);
- entry.__index = yIndex;
- entry.__data.__data = {
- __patches: subPatches,
- __entry: entry
- };
-
- return;
- }
-
- // this key has already been inserted or moved, a duplicate!
- _VirtualDom_insertNode(changes, localPatches, key + _VirtualDom_POSTFIX, vnode, yIndex, inserts);
-}
+// KEYED DIFF
-function _VirtualDom_removeNode(changes, localPatches, key, vnode, index)
+function _VirtualDom_diffKeyedKids(parentDomNode, xParent, yParent, eventNode, tNode)
{
- var entry = changes[key];
-
- // never seen this key before
- if (!entry)
- {
- var patch = _VirtualDom_pushPatch(localPatches, __3_REMOVE, index, undefined);
-
- changes[key] = {
- __tag: __5_REMOVE,
- __vnode: vnode,
- __index: index,
- __data: patch
- };
-
- return;
- }
-
- // this key was inserted earlier, a match!
- if (entry.__tag === __5_INSERT)
- {
- entry.__tag = __5_MOVE;
- var subPatches = [];
- _VirtualDom_diffHelp(vnode, entry.__vnode, subPatches, index);
-
- _VirtualDom_pushPatch(localPatches, __3_REMOVE, index, {
- __patches: subPatches,
- __entry: entry
- });
-
- return;
- }
-
- // this key has already been removed or moved, a duplicate!
- _VirtualDom_removeNode(changes, localPatches, key + _VirtualDom_POSTFIX, vnode, index);
-}
-
-
-
-// ADD DOM NODES
-//
-// Each DOM node has an "index" assigned in order of traversal. It is important
-// to minimize our crawl over the actual DOM, so these indexes (along with the
-// descendantsCount of virtual nodes) let us skip touching entire subtrees of
-// the DOM if we know there are no patches there.
+ var xKids = xParent.__kids;
+ var yKids = yParent.__kids;
+ var xKidsMap = xParent.__kidsMap;
+ var yKidsMap = yParent.__kidsMap;
-function _VirtualDom_addDomNodes(domNode, vNode, patches, eventNode)
-{
- _VirtualDom_addDomNodesHelp(domNode, vNode, patches, 0, 0, vNode.__descendantsCount, eventNode);
-}
+ var xIndexLower = 0;
+ var yIndexLower = 0;
+ var xIndexUpper = xKids.length - 1;
+ var yIndexUpper = yKids.length - 1;
+ var domNodeLower = null;
+ var domNodeUpper = null;
-// assumes `patches` is non-empty and indexes increase monotonically.
-function _VirtualDom_addDomNodesHelp(domNode, vNode, patches, i, low, high, eventNode)
-{
- var patch = patches[i];
- var index = patch.__index;
+ var translated = false;
- while (index === low)
+ var handleDiffReturn = function (diffReturn, upper)
{
- var patchType = patch.$;
+ var domNode = diffReturn.__domNode;
- if (patchType === __3_THUNK)
+ if (diffReturn.__translated)
{
- _VirtualDom_addDomNodes(domNode, vNode.__node, patch.__data, eventNode);
+ translated = true;
}
- else if (patchType === __3_REORDER)
- {
- patch.__domNode = domNode;
- patch.__eventNode = eventNode;
- var subPatches = patch.__data.__patches;
- if (subPatches.length > 0)
+ if (diffReturn.__reinsert)
+ {
+ if (upper)
{
- _VirtualDom_addDomNodesHelp(domNode, vNode, subPatches, 0, low, high, eventNode);
+ _VirtualDom_insertBefore(parentDomNode, domNode, domNodeUpper);
+ domNodeUpper = domNode;
}
- }
- else if (patchType === __3_REMOVE)
- {
- patch.__domNode = domNode;
- patch.__eventNode = eventNode;
-
- var data = patch.__data;
- if (data)
+ else
{
- data.__entry.__data = domNode;
- var subPatches = data.__patches;
- if (subPatches.length > 0)
- {
- _VirtualDom_addDomNodesHelp(domNode, vNode, subPatches, 0, low, high, eventNode);
- }
+ _VirtualDom_insertAfter(parentDomNode, domNode, domNodeLower);
+ domNodeLower = domNode;
}
}
- else
+ // An extension might have removed an element we have rendered before,
+ // or moved it to another parent. In such cases, `parentDomNode.insertBefore(x, domNode)`
+ // and `parentDomNode.moveBefore(x, domNode)` would throw errors. Keep the
+ // previous reference element in those cases – that should still result in the correct
+ // element order, just with some element missing.
+ else if (domNode.parentNode === parentDomNode)
{
- patch.__domNode = domNode;
- patch.__eventNode = eventNode;
- }
-
- i++;
-
- if (!(patch = patches[i]) || (index = patch.__index) > high)
- {
- return i;
+ if (upper)
+ {
+ domNodeUpper = domNode;
+ }
+ else
+ {
+ domNodeLower = domNode;
+ }
}
- }
-
- var tag = vNode.$;
+ };
- if (tag === __2_TAGGER)
+ while (true)
{
- var subNode = vNode.__node;
-
- while (subNode.$ === __2_TAGGER)
+ // Consume from the start until we get stuck.
+ while (xIndexLower <= xIndexUpper && yIndexLower <= yIndexUpper)
{
- subNode = subNode.__node;
- }
+ var xKid = xKids[xIndexLower];
+ var yKid = yKids[yIndexLower];
+ var xKey = xKid.a;
+ var yKey = yKid.a;
+ var x = xKid.b;
+ var y = yKid.b;
+
+ if (xKey === yKey)
+ {
+ var diffReturn = _VirtualDom_diffHelp(x, y, eventNode, tNode.__children[yKey]);
+ xIndexLower++;
+ yIndexLower++;
+ handleDiffReturn(diffReturn, false);
+ continue;
+ }
- return _VirtualDom_addDomNodesHelp(domNode, subNode, patches, i, low + 1, high, domNode.elm_event_node_ref);
- }
+ var xMoved = false;
- // tag must be __2_NODE or __2_KEYED_NODE at this point
+ if (xKey in yKidsMap)
+ {
+ xMoved = true;
+ }
+ else
+ {
+ _VirtualDom_remove(tNode.__children[xKey].__domNode);
+ delete tNode.__children[xKey];
+ xIndexLower++;
+ }
- var vKids = vNode.__kids;
- var childNodes = domNode.childNodes;
- for (var j = 0; j < vKids.length; j++)
- {
- low++;
- var vKid = tag === __2_NODE ? vKids[j] : vKids[j].b;
- var nextLow = low + (vKid.__descendantsCount || 0);
- if (low <= index && index <= nextLow)
- {
- i = _VirtualDom_addDomNodesHelp(childNodes[j], vKid, patches, i, low, nextLow, eventNode);
- if (!(patch = patches[i]) || (index = patch.__index) > high)
+ if (yKey in xKidsMap)
{
- return i;
+ if (xMoved)
+ {
+ break;
+ }
+ }
+ else
+ {
+ var childTNode = _VirtualDom_createTNode(undefined);
+ var domNode = _VirtualDom_render(y, eventNode, childTNode);
+ tNode.__children[yKey] = childTNode;
+ _VirtualDom_insertAfter(parentDomNode, domNode, domNodeLower);
+ yIndexLower++;
+ domNodeLower = domNode;
}
}
- low = nextLow;
- }
- return i;
-}
-
-
-// APPLY PATCHES
-
-
-function _VirtualDom_applyPatches(rootDomNode, oldVirtualNode, patches, eventNode)
-{
- if (patches.length === 0)
- {
- return rootDomNode;
- }
-
- _VirtualDom_addDomNodes(rootDomNode, oldVirtualNode, patches, eventNode);
- return _VirtualDom_applyPatchesHelp(rootDomNode, patches);
-}
-
-function _VirtualDom_applyPatchesHelp(rootDomNode, patches)
-{
- for (var i = 0; i < patches.length; i++)
- {
- var patch = patches[i];
- var localDomNode = patch.__domNode
- var newNode = _VirtualDom_applyPatch(localDomNode, patch);
- if (localDomNode === rootDomNode)
+ // Consume from the end until we get stuck.
+ while (xIndexUpper > xIndexLower && yIndexUpper > yIndexLower)
{
- rootDomNode = newNode;
- }
- }
- return rootDomNode;
-}
-
-function _VirtualDom_applyPatch(domNode, patch)
-{
- switch (patch.$)
- {
- case __3_REDRAW:
- return _VirtualDom_applyPatchRedraw(domNode, patch.__data, patch.__eventNode);
-
- case __3_FACTS:
- _VirtualDom_applyFacts(domNode, patch.__eventNode, patch.__data);
- return domNode;
-
- case __3_TEXT:
- domNode.replaceData(0, domNode.length, patch.__data);
- return domNode;
+ var xKid = xKids[xIndexUpper];
+ var yKid = yKids[yIndexUpper];
+ var xKey = xKid.a;
+ var yKey = yKid.a;
+ var x = xKid.b;
+ var y = yKid.b;
+
+ if (xKey === yKey)
+ {
+ var diffReturn = _VirtualDom_diffHelp(x, y, eventNode, tNode.__children[yKey]);
+ xIndexUpper--;
+ yIndexUpper--;
+ handleDiffReturn(diffReturn, true);
+ continue;
+ }
- case __3_THUNK:
- return _VirtualDom_applyPatchesHelp(domNode, patch.__data);
+ var xMoved = false;
- case __3_TAGGER:
- if (domNode.elm_event_node_ref)
+ if (xKey in yKidsMap)
{
- domNode.elm_event_node_ref.__tagger = patch.__data;
+ xMoved = true;
}
else
{
- domNode.elm_event_node_ref = { __tagger: patch.__data, __parent: patch.__eventNode };
+ _VirtualDom_remove(tNode.__children[xKey].__domNode);
+ delete tNode.__children[xKey];
+ xIndexUpper--;
}
- return domNode;
- case __3_REMOVE_LAST:
- var data = patch.__data;
- for (var i = 0; i < data.__diff; i++)
+ if (yKey in xKidsMap)
{
- domNode.removeChild(domNode.childNodes[data.__length]);
+ if (xMoved)
+ {
+ break;
+ }
}
- return domNode;
-
- case __3_APPEND:
- var data = patch.__data;
- var kids = data.__kids;
- var i = data.__length;
- var theEnd = domNode.childNodes[i];
- for (; i < kids.length; i++)
+ else
{
- domNode.insertBefore(_VirtualDom_render(kids[i], patch.__eventNode), theEnd);
+ var childTNode = _VirtualDom_createTNode(undefined);
+ var domNode = _VirtualDom_render(y, eventNode, childTNode);
+ tNode.__children[yKey] = childTNode;
+ _VirtualDom_insertBefore(parentDomNode, domNode, domNodeUpper);
+ yIndexUpper--;
+ domNodeUpper = domNode;
}
- return domNode;
+ }
- case __3_REMOVE:
- var data = patch.__data;
- if (!data)
+ var swapped = false;
+
+ // Check if the start or end can be unstuck by a swap.
+ if (xIndexLower < xIndexUpper && yIndexLower < yIndexUpper)
+ {
+ var xKidLower = xKids[xIndexLower];
+ var yKidLower = yKids[yIndexLower];
+ var xKidUpper = xKids[xIndexUpper];
+ var yKidUpper = yKids[yIndexUpper];
+
+ var xKeyLower = xKidLower.a;
+ var yKeyLower = yKidLower.a;
+ var xKeyUpper = xKidUpper.a;
+ var yKeyUpper = yKidUpper.a;
+
+ if (xKeyLower === yKeyUpper)
{
- domNode.parentNode.removeChild(domNode);
- return domNode;
+ var diffReturn = _VirtualDom_diffHelp(xKidLower.b, yKidUpper.b, eventNode, tNode.__children[yKeyUpper]);
+ xIndexLower++;
+ yIndexUpper--;
+ _VirtualDom_moveBefore(parentDomNode, diffReturn.__domNode, domNodeUpper);
+ handleDiffReturn(diffReturn, true);
+ swapped = true;
}
- var entry = data.__entry;
- if (typeof entry.__index !== 'undefined')
+
+ if (xKeyUpper === yKeyLower)
{
- domNode.parentNode.removeChild(domNode);
+ var diffReturn = _VirtualDom_diffHelp(xKidUpper.b, yKidLower.b, eventNode, tNode.__children[yKeyLower]);
+ yIndexLower++;
+ xIndexUpper--;
+ _VirtualDom_moveAfter(parentDomNode, diffReturn.__domNode, domNodeLower);
+ handleDiffReturn(diffReturn, false);
+ swapped = true;
}
- entry.__data = _VirtualDom_applyPatchesHelp(domNode, data.__patches);
- return domNode;
-
- case __3_REORDER:
- return _VirtualDom_applyPatchReorder(domNode, patch);
-
- case __3_CUSTOM:
- return patch.__data(domNode);
+ }
- default:
- __Debug_crash(10); // 'Ran into an unknown patch!'
+ // If no swap, stop consuming from start and end.
+ if (!swapped)
+ {
+ break;
+ }
}
-}
-
-
-function _VirtualDom_applyPatchRedraw(domNode, vNode, eventNode)
-{
- var parentNode = domNode.parentNode;
- var newNode = _VirtualDom_render(vNode, eventNode);
- if (!newNode.elm_event_node_ref)
+ // For the remaining items in the new virtual DOM, diff with the corresponding
+ // old virtual DOM node (if any) and move it into the correct place.
+ // This might result in more moves than technically needed, but:
+ // - Moving nodes isn’t that slow. Diffing algorithms aren’t free either.
+ // - In browsers supporting `.moveBefore()` unnecessary moves have no unwanted side effects.
+ // - Elm has never had a “perfect” implementation for Keyed, and this should not
+ // be worse than the previous implementation.
+ for (; yIndexLower <= yIndexUpper; yIndexLower++)
{
- newNode.elm_event_node_ref = domNode.elm_event_node_ref;
+ var yKid = yKids[yIndexLower];
+ var yKey = yKid.a;
+ var y = yKid.b;
+ if (yKey in xKidsMap)
+ {
+ var x = xKidsMap[yKey];
+ var diffReturn = _VirtualDom_diffHelp(x, y, eventNode, tNode.__children[yKey]);
+ _VirtualDom_moveAfter(parentDomNode, diffReturn.__domNode, domNodeLower);
+ handleDiffReturn(diffReturn, false);
+ }
+ else
+ {
+ var childTNode = _VirtualDom_createTNode(undefined);
+ var domNode = _VirtualDom_render(y, eventNode, childTNode);
+ tNode.__children[yKey] = childTNode;
+ _VirtualDom_insertAfter(parentDomNode, domNode, domNodeLower);
+ domNodeLower = domNode;
+ }
}
- if (parentNode && newNode !== domNode)
+ // Remove the remaining old virtual DOM nodes that aren’t present in the new virtual DOM.
+ for (; xIndexLower <= xIndexUpper; xIndexLower++)
{
- parentNode.replaceChild(newNode, domNode);
+ var xKid = xKids[xIndexLower];
+ var xKey = xKid.a;
+ if (!(xKey in yKidsMap)) {
+ _VirtualDom_remove(tNode.__children[xKid.a].__domNode);
+ delete tNode.__children[xKid.a];
+ }
}
- return newNode;
+
+ return translated;
}
+var _VirtualDom_POSTFIX = '_elmW6BL';
-function _VirtualDom_applyPatchReorder(domNode, patch)
+function _VirtualDom_applyPatches(rootDomNode, oldVirtualNode, newVirtualNode, eventNode)
{
- var data = patch.__data;
-
- // remove end inserts
- var frag = _VirtualDom_applyPatchReorderEndInsertsHelp(data.__endInserts, patch);
+ // To avoid making breaking changes to elm/browser, we store the tNode on
+ // the root DOM node instead of returning it.
+ var tNode = rootDomNode.elmTree;
+
+ var diffReturn = _VirtualDom_diffHelp(oldVirtualNode, newVirtualNode, eventNode, tNode);
+ // We can’t do anything about `diffReturn.__translated` or
+ // `diffReturn.__reinsert` here, because we don’t know the parent of the
+ // root node. Note that `rootDomNode.parentNode` cannot be used, because if
+ // the root node is a text node and it has been translated, it is most
+ // likely replaced by other nodes (so the original node is not attached to
+ // the DOM anymore). Returning `Html.text` at the top level of `view` and
+ // expecting it to be translatable is a bit of an edge case anyway.
+ var newDomNode = diffReturn.__domNode;
+
+ if (newDomNode !== rootDomNode)
+ {
+ delete rootDomNode.elmTree;
+ newDomNode.elmTree = tNode;
+ }
- // removals
- domNode = _VirtualDom_applyPatchesHelp(domNode, data.__patches);
+ return newDomNode;
+}
- // inserts
- var inserts = data.__inserts;
- for (var i = 0; i < inserts.length; i++)
+function _VirtualDom_applyPatchRedraw(x, y, eventNode, tNode)
+{
+ var domNode = tNode.__domNode;
+ var parentNode = domNode.parentNode;
+ var isTextNode = domNode.nodeType === 3;
+ var newNode = _VirtualDom_render(y, eventNode, tNode);
+
+ // An extension might have removed the element. In this case, we are redrawing because `x` and `y`
+ // have changed a lot, implying that the structure has changed significantly, and that they can’t
+ // be diffed normally. This means that the extension probably meant to remove the old element, but
+ // not the new one, so return that this element is missing so that it can be re-inserted into the
+ // parent. An example of this is Google Translate: It removes our text nodes and replaces them.
+ // Later we might want to replace that text node with some element.
+ if (parentNode)
{
- var insert = inserts[i];
- var entry = insert.__entry;
- var node = entry.__tag === __5_MOVE
- ? entry.__data
- : _VirtualDom_render(entry.__vnode, patch.__eventNode);
- domNode.insertBefore(node, domNode.childNodes[insert.__index]);
+ parentNode.replaceChild(newNode, domNode);
+ return {
+ __domNode: newNode,
+ __translated: isTextNode && domNode.data !== x.__text,
+ __reinsert: false
+ }
}
-
- // add end inserts
- if (frag)
+ else
{
- _VirtualDom_appendChild(domNode, frag);
+ return {
+ __domNode: newNode,
+ __translated: isTextNode,
+ __reinsert: true
+ }
}
-
- return domNode;
}
+/*
+This is a mapping between attribute names and their corresponding boolean properties,
+and only the ones where the attribute name is different from the property name
+(usually in casing – attributes are case insensitive, and returned lowercase).
+
+The mapping currently only lists the ones that have dedicated functions in elm/html.
+
+There are more though! Running the following code in the console gives more results:
+
+[...new Set(Object.getOwnPropertyNames(window).filter(d => d.startsWith('HTML') || d === 'Node' || d === 'Element' || d === 'EventTarget').flatMap(d => {c = window[d]; m = c.name.match(/^HTML(\w+)Element$/); e = document.createElement(m ? m[1].replace('Anchor', 'a').replace('Paragraph', 'p').replace('Image', 'img').replace('Media', 'video').replace(/^([DOU])List$/, '$1l').toLowerCase() : 'div'); return Object.getOwnPropertyNames(c.prototype).filter(n => typeof e[n] === 'boolean')}))].filter(n => /[A-Z]/.test(n)).sort()
+
+Potential candidates to support (should probably add to elm/html first):
+disablePictureInPicture – video
+playsInline – video
+formNoValidate – button, input
+
+Not useful with Elm:
+noModule – script
+shadowRootClonable – template
+shadowRootDelegatesFocus – template
+shadowRootSerializable – template
+
+Legacy/deprecated:
+allowFullscreen – iframe (use allow="fullscreen" instead)
+allowPaymentRequest – iframe (use allow="payment" instead)
+noHref - area (image maps)
+noResize – frame (not iframe)
+noShade – hr
+trueSpeed – marquee
+
+Special:
+defaultChecked
+defaultMuted
+defaultSelected
+
+No corresponding attribute:
+disableRemotePlayback
+isConnected
+isContentEditable
+preservesPitch
+sharedStorageWritable
+willValidate
+
+Unclear:
+adAuctionHeaders
+browsingTopics
+
+Regarding the special ones: `` results in `.defaultChecked ===
+true`. Similarly, setting `input.defaultChecked = true` results in
+`input.outerHTML === ''`. `input.checked = true` does _not_
+result in an attribute though: `.checked` has no corresponding attribute.
+However, when serializing `Html.input [ Html.Attributes.checked True ] []` to
+HTML, `` is the most reasonable choice. So when virtualizing, we
+actually want to turn the `checked` attribute back into a boolean "checked"
+property in Elm (even if according to the DOM, it's `.defaultChecked`). Same
+thing for `muted` and `selected`.
+*/
+var _VirtualDom_camelCaseBoolProperties = {
+ novalidate: 'noValidate',
+ readonly: 'readOnly',
+ ismap: 'isMap'
+};
+
+// Used for server side rendering to keep track of which elements to
+// virtualize. This is added to _all_ nodes (except text nodes) in
+// `_VirtualDom_organizeFacts`. Server side rendering renders _all_ string and
+// boolean facts as attributes, including this one. `_VirtualDom_applyProps`
+// and `_VirtualDom_removeProps` _ignore_ this property, in order not to
+// clutter the browser dev tools. `_VirtualDom_virtualize` only virtualizes
+// children with this attribute. This way it knows which elements are “ours”
+// and which were inserted by third-party scripts (before the virtualization
+// took place). The root node is allowed not to have this attribute though, in
+// order not to force everyone to put this attribute on the node they mount the
+// Elm app on. During the first render after virtualization, we remove this
+// attribute from all elements, to unclutter the browser console. That happens
+// via `_VirtualDom_virtualize` virtualizing it as an _attribute_ (not a
+// property) which, when compared to the result of `view`, is diffed for
+// removal.
+var _VirtualDom_markerProperty = 'data-elm';
-function _VirtualDom_applyPatchReorderEndInsertsHelp(endInserts, patch)
+function _VirtualDom_virtualize(node)
{
- if (!endInserts)
+ // The debugger has always done `_VirtualDom_virtualize(document)` instead of
+ // `_VirtualDom_virtualize(document.body)` by mistake. To be backwards compatible
+ // with elm/browser, support that here.
+ if (node === _VirtualDom_doc)
{
- return;
+ node = _VirtualDom_doc.body;
}
- var frag = _VirtualDom_doc.createDocumentFragment();
- for (var i = 0; i < endInserts.length; i++)
+ if (node.elmTree)
{
- var insert = endInserts[i];
- var entry = insert.__entry;
- _VirtualDom_appendChild(frag, entry.__tag === __5_MOVE
- ? entry.__data
- : _VirtualDom_render(entry.__vnode, patch.__eventNode)
- );
+ // The `console.error` lets the user more easily identify which node they passed.
+ console.error('node.elmTree already exists:', node.elmTree, node);
+ throw new Error('node.elmTree already exists!');
}
- return frag;
-}
+ var tNode = _VirtualDom_createTNode(node);
-function _VirtualDom_virtualize(node)
+ // Fall back to a text node as backwards compatibility. Elm has always
+ // supported mounting onto any node, even comment nodes. Text nodes,
+ // comment nodes, CDATA sections and processing instructions all implement
+ // the `CharacterData` abstract interface, so representing them as a text
+ // node should be fine. The whole document, doctypes and document fragments
+ // are also nodes, but they are increasingly silly to render into and have
+ // never worked with Elm.
+ var vNode = _VirtualDom_virtualizeHelp(node, tNode) || _VirtualDom_text('');
+
+ node.elmTree = tNode;
+
+ return vNode;
+}
+
+function _VirtualDom_virtualizeHelp(node, tNode)
{
// TEXT NODES
@@ -1541,12 +1833,13 @@ function _VirtualDom_virtualize(node)
if (node.nodeType !== 1)
{
- return _VirtualDom_text('');
+ return undefined;
}
// ELEMENT NODES
+ var tag = node.localName;
var attrList = __List_Nil;
var attrs = node.attributes;
for (var i = attrs.length; i--; )
@@ -1554,29 +1847,203 @@ function _VirtualDom_virtualize(node)
var attr = attrs[i];
var name = attr.name;
var value = attr.value;
- attrList = __List_Cons( A2(_VirtualDom_attribute, name, value), attrList );
+
+ // The `style` attribute and `node.style` are linked. While `node.style` contains
+ // every single CSS property, it’s possible to loop over only the styles that have
+ // been set via `node.style.length`. Unfortunately, `node.style` expands shorthand
+ // properties and normalizes values. For example, `padding: 0` is turned into
+ // `padding-top: 0px; padding-bottom: 0px; ...`.
+ // The best bet is actually parsing the styles ourselves. Naively splitting on `;`
+ // is not 100 % correct, for example it won’t work for `content: ";"`. It will work
+ // in 99 % of cases though, since putting a semicolon in a value isn’t that common.
+ // And even in those cases, nothing will break. We’ll just apply a few styles
+ // unnecessarily at init.
+ if (name === "style")
+ {
+ var parts = value.split(";");
+ for (var j = parts.length; j--; )
+ {
+ var part = parts[j];
+ var index = part.indexOf(":");
+ if (index !== -1)
+ {
+ var cssKey = part.slice(0, index).trim();
+ var cssValue = part.slice(index + 1).trim();
+ attrList = __List_Cons(A2(_VirtualDom_style, cssKey, cssValue), attrList);
+ }
+ }
+ continue;
+ }
+
+ var namespaceURI = attr.namespaceURI;
+ var propertyName = _VirtualDom_camelCaseBoolProperties[name] || name;
+ var propertyValue = node[propertyName];
+ // Turning attributes into virtual DOM representations is not an exact science.
+ // If someone runs an Elm `view` function and then serializes it to HTML, we need to guess:
+ //
+ // - how they chose to serialize it
+ // - what the most likely virtual DOM representation is
+ //
+ // In elm/html, the convention is to use attributes rather than properties where possible,
+ // which is good for virtualization – we can just turn most HTML attributes we find as-is
+ // into virtual DOM attributes. But when we encounter `foo="bar"` we can’t know if it was
+ // created using `Html.Attributes.attribute "foo" "bar"` or
+ // `Html.Attributes.property "foo" (Json.Encode.string "bar")`.
+ //
+ // It's not the end of the world if we guess wrong, though, it just leads to a bit of
+ // unnecessary DOM mutations on the first render.
+ //
+ // Do we need to use any of the functions in the “XSS ATTACK VECTOR CHECKS”
+ // section while virtualizing? I don’t think so, because they will already
+ // have executed at this point, and the first render will remove any disallowed
+ // attributes.
+ attrList = __List_Cons(
+ // `Html.Attributes.value` sets the `.value` property to a string, because that’s the
+ // only way to set the value of an input element. The `.value` property has no corresponding
+ // attribute; the `value` attribute maps to the `.defaultValue` property. But when serializing,
+ // the most likely way to do it is to serialize the `.value` property to the `value` attribute.
+ name === 'value'
+ ? A2(_VirtualDom_property, name, value)
+ :
+ // Try to guess if the attribute comes from one of the functions
+ // implemented using `boolProperty` in `Html.Attributes`.
+ // See `Html.Attributes.spellcheck` for that exception.
+ typeof propertyValue === 'boolean' && name !== 'spellcheck'
+ ? A2(_VirtualDom_property, propertyName, propertyValue)
+ :
+ // Otherwise, guess that it is an attribute. The user might have used `Html.Attributes.property`,
+ // but there’s no way for us to know that.
+ namespaceURI
+ ? A3(_VirtualDom_attributeNS, namespaceURI, name, value)
+ : A2(_VirtualDom_attribute, name, value),
+ attrList
+ );
}
- var tag = node.tagName.toLowerCase();
- var kidList = __List_Nil;
- var kids = node.childNodes;
+ var namespace =
+ node.namespaceURI === 'http://www.w3.org/1999/xhtml'
+ ? undefined
+ : node.namespaceURI;
+ var kidList = [];
+
+ // To create a text area with default text in HTML:
+ // - correct:
+ // - wrong: (value="default text" does nothing.)
+ // In the DOM, that becomes an `HTMLTextAreaElement`, with `.value === "default text"`.
+ // It contains a single text node with the text `"default text"` too.
+ // When the user types into the text area, `.value` changes, but the inner text node stays unchanged.
+ // In Elm, you need to use `Html.textarea [ Html.Attributes.value myValue ] []` to be able to set the value.
+ // All in all, this means that the most useful virtualization is:
+ // - Skip any children (most likely a single text node), because the Elm code most likely set none.
+ // - Pick up `.value`, even though it wasn’t set as an attribute in HTML – but most likely is a property set by the Elm code.
+ // Note that in