mirror of
https://github.com/marko-js/marko.git
synced 2025-12-08 19:26:05 +00:00
366 lines
11 KiB
JavaScript
366 lines
11 KiB
JavaScript
require('raptor-polyfill/string/startsWith');
|
|
var inherit = require('raptor-util/inherit');
|
|
var extend = require('raptor-util/extend');
|
|
var Text = require('./Text');
|
|
var Comment = require('./Comment');
|
|
var Node = require('./Node');
|
|
var documentProvider = require('../document-provider');
|
|
var virtualizeHTML;
|
|
|
|
var NS_XLINK = 'http://www.w3.org/1999/xlink';
|
|
var ATTR_HREF = 'href';
|
|
var EMPTY_OBJECT = require('./util').EMPTY_OBJECT;
|
|
var ATTR_MARKO_CONST = 'data-marko-const';
|
|
|
|
function removePreservedAttributes(attrs) {
|
|
var preservedAttrs = attrs['data-preserve-attrs'];
|
|
if (preservedAttrs) {
|
|
for (var i=0, len=preservedAttrs.length; i<len; i++) {
|
|
var preservedAttrName = preservedAttrs[i];
|
|
delete attrs[preservedAttrName];
|
|
}
|
|
}
|
|
return attrs;
|
|
}
|
|
|
|
function convertAttrValue(type, value) {
|
|
if (value === true) {
|
|
return '';
|
|
} else if (type === 'object') {
|
|
return JSON.stringify(value);
|
|
} else {
|
|
return value.toString();
|
|
}
|
|
}
|
|
|
|
function HTMLElementClone(other) {
|
|
extend(this, other);
|
|
this.parentNode = undefined;
|
|
this._nextSibling = undefined;
|
|
}
|
|
|
|
function HTMLElement(tagName, attrs, childCount, constId) {
|
|
var namespaceURI;
|
|
var isTextArea;
|
|
|
|
switch(tagName) {
|
|
case 'svg':
|
|
namespaceURI = 'http://www.w3.org/2000/svg';
|
|
break;
|
|
case 'math':
|
|
namespaceURI = 'http://www.w3.org/1998/Math/MathML';
|
|
break;
|
|
case 'textarea':
|
|
case 'TEXTAREA':
|
|
isTextArea = true;
|
|
break;
|
|
}
|
|
|
|
Node.call(this, childCount);
|
|
|
|
if (constId) {
|
|
if (!attrs) {
|
|
attrs = {};
|
|
}
|
|
attrs[ATTR_MARKO_CONST] = constId;
|
|
}
|
|
|
|
this.attributes = attrs || EMPTY_OBJECT;
|
|
this._isTextArea = isTextArea;
|
|
this.namespaceURI = namespaceURI;
|
|
this.nodeName = tagName;
|
|
this._value = undefined;
|
|
this._constId = constId;
|
|
}
|
|
|
|
HTMLElement.prototype = {
|
|
nodeType: 1,
|
|
|
|
_nsAware: true,
|
|
|
|
assignAttributes: function(targetNode) {
|
|
var attrs = this.attributes;
|
|
var attrName;
|
|
var i;
|
|
|
|
// We use expando properties to associate the previous HTML
|
|
// attributes provided as part of the VDOM node with the
|
|
// real HTMLElement 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 HTMLElement node will not have the expando property
|
|
// so we build the attribute map from the expando property
|
|
|
|
var oldAttrs = targetNode._vattrs;
|
|
if (oldAttrs) {
|
|
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;
|
|
}
|
|
} else {
|
|
// We need to build the attribute map from the real attributes
|
|
oldAttrs = {};
|
|
|
|
var oldAttributesList = targetNode.attributes;
|
|
for (i = oldAttributesList.length - 1; i >= 0; --i) {
|
|
var attr = oldAttributesList[i];
|
|
|
|
if (attr.specified !== false) {
|
|
attrName = attr.name;
|
|
var attrNamespaceURI = attr.namespaceURI;
|
|
if (attrNamespaceURI === NS_XLINK) {
|
|
oldAttrs['xlink:href'] = attr.value;
|
|
} else {
|
|
oldAttrs[attrName] = attr.value;
|
|
}
|
|
}
|
|
}
|
|
|
|
// We don't want preserved attributes to show up in either the old
|
|
// or new attribute map.
|
|
removePreservedAttributes(oldAttrs);
|
|
}
|
|
|
|
// In some cases we only want to set an attribute value for the first
|
|
// render or we don't want certain attributes to be touched. To support
|
|
// that use case we delete out all of the preserved attributes
|
|
// so it's as if they never existed.
|
|
var preservedAttrs = attrs['data-preserve-attrs'];
|
|
if (preservedAttrs) {
|
|
attrs = removePreservedAttributes(extend({}, attrs));
|
|
}
|
|
|
|
// 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) {
|
|
var attrValue = attrs[attrName];
|
|
|
|
if (attrName === 'xlink:href') {
|
|
if (attrValue == null || attrValue === false) {
|
|
targetNode.removeAttributeNS(NS_XLINK, ATTR_HREF);
|
|
} else if (oldAttrs[attrName] !== attrValue) {
|
|
targetNode.setAttributeNS(NS_XLINK, ATTR_HREF, attrValue);
|
|
}
|
|
} else {
|
|
if (attrValue == null || attrValue === false) {
|
|
targetNode.removeAttribute(attrName);
|
|
} else if (oldAttrs[attrName] !== attrValue) {
|
|
|
|
if (attrName.startsWith('data-_')) {
|
|
// Special attributes aren't copied to the real DOM. They are only
|
|
// kept in the virtual attributes map
|
|
continue;
|
|
}
|
|
|
|
var type = typeof attrValue;
|
|
|
|
if (type !== 'string') {
|
|
attrValue = convertAttrValue(type, attrValue);
|
|
}
|
|
|
|
targetNode.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
|
|
for (attrName in oldAttrs) {
|
|
if (attrs.hasOwnProperty(attrName) === false) {
|
|
if (attrName === 'xlink:href') {
|
|
targetNode.removeAttributeNS(NS_XLINK, ATTR_HREF);
|
|
} else {
|
|
targetNode.removeAttribute(attrName);
|
|
}
|
|
}
|
|
}
|
|
|
|
targetNode._vattrs = attrs;
|
|
},
|
|
|
|
cloneNode: function() {
|
|
return new HTMLElementClone(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, childCount, constId) {
|
|
var child = this.appendChild(new HTMLElement(tagName, attrs, childCount, constId));
|
|
|
|
if (childCount === 0) {
|
|
return this._finishChild();
|
|
} else {
|
|
return child;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Shorthand method for creating and appending a Text node with a given value
|
|
* @param {String} value The text value for the new Text node
|
|
*/
|
|
t: function(value) {
|
|
var type = typeof value;
|
|
|
|
if (type !== 'string') {
|
|
if (value == null) {
|
|
value = '';
|
|
} else if (type === 'object') {
|
|
var safeHTML = value.safeHTML;
|
|
var vdomNode = virtualizeHTML(safeHTML || '', documentProvider.document);
|
|
this.appendChild(vdomNode);
|
|
return this._finishChild();
|
|
} else {
|
|
value = value.toString();
|
|
}
|
|
}
|
|
this.appendChild(new Text(value));
|
|
return this._finishChild();
|
|
},
|
|
|
|
/**
|
|
* Shorthand method for creating and appending a Comment node with a given value
|
|
* @param {String} value The value for the new Comment node
|
|
*/
|
|
c: function(value) {
|
|
this.appendChild(new Comment(value));
|
|
return this._finishChild();
|
|
},
|
|
|
|
/**
|
|
* 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) {
|
|
this.appendChild(node.cloneNode());
|
|
return this._finishChild();
|
|
},
|
|
|
|
actualize: function(document) {
|
|
var el;
|
|
var namespaceURI = this.namespaceURI;
|
|
var tagName = this.nodeName;
|
|
|
|
if (namespaceURI) {
|
|
el = document.createElementNS(namespaceURI, tagName);
|
|
} else {
|
|
el = document.createElement(tagName);
|
|
}
|
|
|
|
var attributes = this.attributes;
|
|
for (var attrName in attributes) {
|
|
var attrValue = attributes[attrName];
|
|
|
|
if (attrName.startsWith('data-_')) {
|
|
continue;
|
|
}
|
|
|
|
if (attrValue !== false && attrValue != null) {
|
|
var type = typeof attrValue;
|
|
|
|
if (type !== 'string') {
|
|
// Special attributes aren't copied to the real DOM. They are only
|
|
// kept in the virtual attributes map
|
|
attrValue = convertAttrValue(type, attrValue);
|
|
}
|
|
|
|
if (attrName === 'xlink:href') {
|
|
el.setAttributeNS(NS_XLINK, ATTR_HREF, attrValue);
|
|
} else {
|
|
el.setAttribute(attrName, attrValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this._isTextArea) {
|
|
el.value = this.value;
|
|
} else {
|
|
var curChild = this.firstChild;
|
|
|
|
while(curChild) {
|
|
el.appendChild(curChild.actualize(document));
|
|
curChild = curChild.nextSibling;
|
|
}
|
|
}
|
|
|
|
el._vattrs = attributes;
|
|
|
|
return el;
|
|
},
|
|
|
|
hasAttributeNS: function(namespaceURI, name) {
|
|
// We don't care about the namespaces since the there
|
|
// is no chance that attributes with the same name will have
|
|
// different namespaces
|
|
return this.attributes[name] !== undefined;
|
|
},
|
|
|
|
getAttribute: function(name) {
|
|
return this.attributes[name];
|
|
},
|
|
|
|
isSameNode: function(otherNode) {
|
|
if (otherNode.nodeType !== 1) {
|
|
return false;
|
|
}
|
|
|
|
var constId = this._constId;
|
|
if (constId) {
|
|
var otherSameId = otherNode.actualize ? otherNode._constId : otherNode.getAttribute(ATTR_MARKO_CONST);
|
|
return constId === otherSameId;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
};
|
|
|
|
inherit(HTMLElement, Node);
|
|
|
|
var proto = HTMLElementClone.prototype = HTMLElement.prototype;
|
|
|
|
Object.defineProperty(proto, 'checked', {
|
|
get: function () {
|
|
return this.attributes.checked !== undefined;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(proto, 'selected', {
|
|
get: function () {
|
|
return this.attributes.selected !== undefined;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(proto, 'id', {
|
|
get: function () {
|
|
return this.attributes.id;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(proto, 'value', {
|
|
get: function () {
|
|
return this._value || this.attributes.value;
|
|
},
|
|
set: function (value) {
|
|
this._value = value;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(proto, 'disabled', {
|
|
get: function () {
|
|
return this.attributes.disabled !== undefined;
|
|
}
|
|
});
|
|
|
|
module.exports = HTMLElement;
|
|
|
|
virtualizeHTML = require('./virtualizeHTML'); |