marko
Version:
UI Components + streaming, async, high performance, HTML templating for Node.js and the browser.
375 lines (326 loc) • 10.4 kB
JavaScript
// eslint-disable-next-line no-constant-binary-expression
var complain = "MARKO_DEBUG" && require("complain");
var inherit = require("raptor-util/inherit");
var componentsUtil = require("@internal/components-util");
var domData = require("../components/dom-data");
var vElementByDOMNode = domData.___vElementByDOMNode;
var VNode = require("./VNode");
var isTextOnly = require("./is-text-only");
var ATTR_XLINK_HREF = "xlink:href";
var xmlnsRegExp = /^xmlns(:|$)/;
var hasOwnProperty = Object.prototype.hasOwnProperty;
var NS_XLINK = "http://www.w3.org/1999/xlink";
var NS_HTML = "http://www.w3.org/1999/xhtml";
var NS_MATH = "http://www.w3.org/1998/Math/MathML";
var NS_SVG = "http://www.w3.org/2000/svg";
var DEFAULT_NS = {
svg: NS_SVG,
math: NS_MATH,
};
var FLAG_SIMPLE_ATTRS = 1;
var FLAG_CUSTOM_ELEMENT = 2;
var FLAG_SPREAD_ATTRS = 4;
var ATTR_HREF = "href";
var EMPTY_OBJECT = Object.freeze(Object.create(null));
var specialElHandlers = {
option: {
selected: function (fromEl, value) {
fromEl.selected = value !== undefined;
},
},
input: {
value: function (fromEl, value) {
fromEl.value = value === undefined ? "" : value;
},
checked: function (fromEl, value) {
fromEl.checked = value !== undefined;
},
},
};
function normalizeValue(value) {
if (value === true) {
return "";
}
if (value == null || value === false) {
return;
}
switch (typeof value) {
case "string":
return value;
case "object":
switch (value.toString) {
case Object.prototype.toString:
case Array.prototype.toString:
// eslint-disable-next-line no-constant-condition
if ("MARKO_DEBUG") {
complain(
"Relying on JSON.stringify for attribute values is deprecated, in future versions of Marko these will be cast to strings instead.",
);
}
return JSON.stringify(value);
case RegExp.prototype.toString:
return value.source;
}
break;
}
return value + "";
}
function assign(a, b) {
for (var key in b) {
if (hasOwnProperty.call(b, key)) {
a[key] = b[key];
}
}
}
function VElementClone(other) {
this.___firstChildInternal = other.___firstChildInternal;
this.___parentNode = null;
this.___nextSiblingInternal = null;
this.___key = other.___key;
this.___attributes = other.___attributes;
this.___properties = other.___properties;
this.___nodeName = other.___nodeName;
this.___flags = other.___flags;
this.___textContent = other.___textContent;
this.___constId = other.___constId;
}
function VElement(
tagName,
attrs,
key,
ownerComponent,
childCount,
flags,
props,
) {
this.___VNode(childCount, ownerComponent);
var constId;
if (props) {
constId = props.i;
}
this.___key = key;
this.___flags = flags || 0;
this.___attributes = attrs || EMPTY_OBJECT;
this.___properties = props || EMPTY_OBJECT;
this.___nodeName = tagName;
this.___textContent = "";
this.___constId = constId;
this.___preserve = false;
this.___preserveBody = false;
}
VElement.prototype = {
___nodeType: 1,
___cloneNode: function () {
return new VElementClone(this);
},
/**
* Shorthand method for creating and appending an HTML element
*
* @param {String} tagName The tag name (e.g. "div")
* @param {int|null} attrCount The number of attributes (or `null` if not known)
* @param {int|null} childCount The number of child nodes (or `null` if not known)
*/
e: function (tagName, attrs, key, ownerComponent, childCount, flags, props) {
var child = this.___appendChild(
new VElement(
tagName,
attrs,
key,
ownerComponent,
childCount,
flags,
props,
),
);
if (childCount === 0) {
return this.___finishChild();
} else {
return child;
}
},
/**
* Shorthand method for creating and appending a static node. The provided node is automatically cloned
* using a shallow clone since it will be mutated as a result of setting `nextSibling` and `parentNode`.
*
* @param {String} value The value for the new Comment node
*/
n: function (node, ownerComponent) {
node = node.___cloneNode();
node.___ownerComponent = ownerComponent;
this.___appendChild(node);
return this.___finishChild();
},
___actualize: function (host, parentNamespaceURI) {
var tagName = this.___nodeName;
var attributes = this.___attributes;
var namespaceURI = DEFAULT_NS[tagName] || parentNamespaceURI || NS_HTML;
var flags = this.___flags;
var el = (host.ownerDocument || host).createElementNS(
namespaceURI,
tagName,
);
if (flags & FLAG_CUSTOM_ELEMENT) {
assign(el, attributes);
} else {
for (var attrName in attributes) {
var attrValue = normalizeValue(attributes[attrName]);
if (attrValue !== undefined) {
if (attrName == ATTR_XLINK_HREF) {
el.setAttributeNS(NS_XLINK, ATTR_HREF, attrValue);
} else {
el.setAttribute(attrName, attrValue);
}
}
}
if (isTextOnly(tagName)) {
el.textContent = this.___textContent;
}
}
vElementByDOMNode.set(el, this);
return el;
},
};
inherit(VElement, VNode);
VElementClone.prototype = VElement.prototype;
function virtualizeElement(node, virtualizeChildNodes, ownerComponent) {
var attributes = node.attributes;
var attrCount = attributes.length;
var attrs = null;
var props = null;
if (attrCount) {
attrs = {};
for (var i = 0; i < attrCount; i++) {
var attr = attributes[i];
var attrName = attr.name;
if (!xmlnsRegExp.test(attrName)) {
if (attrName === "data-marko") {
props = componentsUtil.___getMarkoPropsFromEl(node);
} else if (attr.namespaceURI === NS_XLINK) {
attrs[ATTR_XLINK_HREF] = attr.value;
} else {
attrs[attrName] = attr.value;
}
}
}
}
var tagName = node.nodeName;
if (node.namespaceURI === NS_HTML) {
tagName = tagName.toLowerCase();
}
var vdomEl = new VElement(
tagName,
attrs,
null /*key*/,
ownerComponent,
0 /*child count*/,
0 /*flags*/,
props,
);
if (isTextOnly(tagName)) {
vdomEl.___textContent = node.textContent;
} else if (virtualizeChildNodes) {
virtualizeChildNodes(node, vdomEl, ownerComponent);
}
return vdomEl;
}
VElement.___virtualize = virtualizeElement;
VElement.___morphAttrs = function (fromEl, vFromEl, toEl) {
var fromFlags = vFromEl.___flags;
var toFlags = toEl.___flags;
var attrs = toEl.___attributes;
if (toFlags & FLAG_CUSTOM_ELEMENT) {
return assign(fromEl, attrs);
}
var props = toEl.___properties;
var attrName;
// We use expando properties to associate the previous HTML
// attributes provided as part of the VDOM node with the
// real VElement DOM node. When diffing attributes,
// we only use our internal representation of the attributes.
// When diffing for the first time it's possible that the
// real VElement node will not have the expando property
// so we build the attribute map from the expando property
var oldAttrs = vFromEl.___attributes;
if (oldAttrs === attrs) {
// For constant attributes the same object will be provided
// every render and we can use that to our advantage to
// not waste time diffing a constant, immutable attribute
// map.
return;
}
var attrValue;
if (toFlags & FLAG_SIMPLE_ATTRS && fromFlags & FLAG_SIMPLE_ATTRS) {
if (oldAttrs["class"] !== (attrValue = attrs["class"])) {
if (attrValue) {
fromEl.className = attrValue;
} else {
fromEl.removeAttribute("class");
}
}
if (oldAttrs.id !== (attrValue = attrs.id)) {
if (attrValue) {
fromEl.id = attrValue;
} else {
fromEl.removeAttribute("id");
}
}
if (oldAttrs.style !== (attrValue = attrs.style)) {
if (attrValue) {
fromEl.style.cssText = attrValue;
} else {
fromEl.removeAttribute("style");
}
}
return;
}
var preserve = (props && props.pa) || EMPTY_OBJECT;
var specialAttrs = specialElHandlers[toEl.___nodeName] || EMPTY_OBJECT;
var specialAttr;
// Loop over all of the attributes in the attribute map and compare
// them to the value in the old map. However, if the value is
// null/undefined/false then we want to remove the attribute
for (attrName in attrs) {
if (!preserve[attrName]) {
attrValue = normalizeValue(attrs[attrName]);
if ((specialAttr = specialAttrs[attrName])) {
specialAttr(fromEl, attrValue);
} else if (normalizeValue(oldAttrs[attrName]) !== attrValue) {
if (attrName === ATTR_XLINK_HREF) {
if (attrValue === undefined) {
fromEl.removeAttributeNS(NS_XLINK, ATTR_HREF);
} else {
fromEl.setAttributeNS(NS_XLINK, ATTR_HREF, attrValue);
}
} else if (attrValue === undefined) {
fromEl.removeAttribute(attrName);
} else {
fromEl.setAttribute(attrName, attrValue);
}
}
}
}
// If there are any old attributes that are not in the new set of attributes
// then we need to remove those attributes from the target node
//
// NOTE: We can skip this if the the element is keyed and didn't have spread attributes
// because we know we already processed all of the attributes for
// both the target and original element since target VElement nodes will
// have all attributes declared. However, we can only skip if the node
// was not a virtualized node (i.e., a node that was not rendered by a
// Marko template, but rather a node that was created from an HTML
// string or a real DOM node).
if (toEl.___key === null || fromFlags & FLAG_SPREAD_ATTRS) {
for (attrName in oldAttrs) {
if (!(attrName in attrs)) {
if ((specialAttr = specialAttrs[attrName])) {
specialAttr(fromEl, undefined);
} else if (attrName === ATTR_XLINK_HREF) {
fromEl.removeAttributeNS(ATTR_XLINK_HREF, ATTR_HREF);
} else {
fromEl.removeAttribute(attrName);
}
}
}
}
};
module.exports = VElement;