UNPKG

marko

Version:

UI Components + streaming, async, high performance, HTML templating for Node.js and the browser.

374 lines (320 loc) • 9.6 kB
"use strict"; // eslint-disable-next-line no-constant-binary-expression var inherit = require("raptor-util/inherit"); var componentsUtil = require("@internal/components-util"); var domData = require("../components/dom-data"); var vElementByDOMNode = domData._M_; 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 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.ce_ = other.ce_; this.cb_ = null; this.cc_ = null; this.ca_ = other.ca_; this.cf_ = other.cf_; this._Q_ = other._Q_; this.cg_ = other.cg_; this.u_ = other.u_; this.ch_ = other.ch_; this.ci_ = other.ci_; } function VElement( tagName, attrs, key, ownerComponent, childCount, flags, props) { this.bY_(childCount, ownerComponent); var constId; if (props) { constId = props.i; } this.ca_ = key; this.u_ = flags || 0; this.cf_ = attrs || EMPTY_OBJECT; this._Q_ = props || EMPTY_OBJECT; this.cg_ = tagName; this.ch_ = ""; this.ci_ = constId; this.af_ = false; this.ae_ = false; } VElement.prototype = { c__: 1, bS_: 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.bR_( new VElement( tagName, attrs, key, ownerComponent, childCount, flags, props ) ); if (childCount === 0) { return this.cj_(); } 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.bS_(); node._O_ = ownerComponent; this.bR_(node); return this.cj_(); }, br_: function (host, parentNamespaceURI) { var tagName = this.cg_; var attributes = this.cf_; var namespaceURI = DEFAULT_NS[tagName] || parentNamespaceURI || NS_HTML; var flags = this.u_; 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.ch_; } } 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._t_(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.ch_ = node.textContent; } else if (virtualizeChildNodes) { virtualizeChildNodes(node, vdomEl, ownerComponent); } return vdomEl; } VElement.ck_ = virtualizeElement; VElement.cl_ = function (fromEl, vFromEl, toEl) { var fromFlags = vFromEl.u_; var toFlags = toEl.u_; var attrs = toEl.cf_; if (toFlags & FLAG_CUSTOM_ELEMENT) { return assign(fromEl, attrs); } var props = toEl._Q_; 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.cf_; 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.cg_] || 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.ca_ === 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;