UNPKG

@amarajs/plugin-dom

Version:

Provides virtual DOM for AmaraJS web applications.

252 lines (215 loc) 5.93 kB
/** * index.js * * A client-side DOM to vdom parser based on DOMParser API */ 'use strict'; var VNode = require('virtual-dom/vnode/vnode'); var VText = require('virtual-dom/vnode/vtext'); var domParser; var propertyMap = require('./property-map'); var namespaceMap = require('./namespace-map'); var HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'; module.exports = parser; /** * DOM/html string to vdom parser * * @param Mixed el DOM element or html string * @param String attr Attribute name that contains vdom key * @return Object VNode or VText */ function parser(el, attr) { // empty input fallback to empty text node if (!el) { return createNode(document.createTextNode('')); } if (typeof el === 'string') { if ( !('DOMParser' in window) ) { throw new Error('DOMParser is not available, so parsing string to DOM node is not possible.'); } domParser = domParser || new DOMParser(); var doc = domParser.parseFromString(el, 'text/html'); // most tags default to body if (doc.body.firstChild) { el = doc.getElementsByTagName('body')[0].firstChild; // some tags, like script and style, default to head } else if (doc.head.firstChild && (doc.head.firstChild.tagName !== 'TITLE' || doc.title)) { el = doc.head.firstChild; // special case for html comment, cdata, doctype } else if (doc.firstChild && doc.firstChild.tagName !== 'HTML') { el = doc.firstChild; // other element, such as whitespace, or html/body/head tag, fallback to empty text node } else { el = document.createTextNode(''); } } if (typeof el !== 'object' || !el || !el.nodeType) { throw new Error('invalid dom node', el); } return createNode(el, attr); } /** * Create vdom from dom node * * @param Object el DOM element * @param String attr Attribute name that contains vdom key * @return Object VNode or VText */ function createNode(el, attr) { // html comment is not currently supported by virtual-dom if (el.nodeType === 3) { return createVirtualTextNode(el); // cdata or doctype is not currently supported by virtual-dom } else if (el.nodeType === 1 || el.nodeType === 9) { return createVirtualDomNode(el, attr); } // default to empty text node return new VText(''); } /** * Create vtext from dom node * * @param Object el Text node * @return Object VText */ function createVirtualTextNode(el) { return new VText(el.nodeValue); } /** * Create vnode from dom node * * @param Object el DOM element * @param String attr Attribute name that contains vdom key * @return Object VNode */ function createVirtualDomNode(el, attr) { var ns = el.namespaceURI !== HTML_NAMESPACE ? el.namespaceURI : null; var key = attr && el.getAttribute(attr) ? el.getAttribute(attr) : null; return new VNode( el.tagName , createProperties(el) , createChildren(el, attr) , key , ns ); } /** * Recursively create vdom * * @param Object el Parent element * @param String attr Attribute name that contains vdom key * @return Array Child vnode or vtext */ function createChildren(el, attr) { var children = []; for (var i = 0; i < el.childNodes.length; i++) { children.push(createNode(el.childNodes[i], attr)); }; return children; } /** * Create properties from dom node * * @param Object el DOM element * @return Object Node properties and attributes */ function createProperties(el) { var properties = {}; if (!el.hasAttributes()) { return properties; } var ns; if (el.namespaceURI && el.namespaceURI !== HTML_NAMESPACE) { ns = el.namespaceURI; } var attr; for (var i = 0; i < el.attributes.length; i++) { // use built in css style parsing if(el.attributes[i].name == 'style'){ attr = createStyleProperty(el); } else if (ns) { attr = createPropertyNS(el.attributes[i]); } else { attr = createProperty(el.attributes[i]); } // special case, namespaced attribute, use properties.foobar if (attr.ns) { properties[attr.name] = { namespace: attr.ns , value: attr.value }; // special case, use properties.attributes.foobar } else if (attr.isAttr) { // init attributes object only when necessary if (!properties.attributes) { properties.attributes = {} } properties.attributes[attr.name] = attr.value; // default case, use properties.foobar } else { properties[attr.name] = attr.value; } }; return properties; } /** * Create property from dom attribute * * @param Object attr DOM attribute * @return Object Normalized attribute */ function createProperty(attr) { var name, value, isAttr; // using a map to find the correct case of property name if (propertyMap[attr.name]) { name = propertyMap[attr.name]; } else { name = attr.name; } // special cases for data attribute, we default to properties.attributes.data if (name.indexOf('data-') === 0 || name.indexOf('aria-') === 0) { value = attr.value; isAttr = true; } else { value = attr.value; } return { name: name , value: value , isAttr: isAttr || false }; } /** * Create namespaced property from dom attribute * * @param Object attr DOM attribute * @return Object Normalized attribute */ function createPropertyNS(attr) { var name, value; return { name: attr.name , value: attr.value , ns: namespaceMap[attr.name] || '' }; } /** * Create style property from dom node * * @param Object el DOM node * @return Object Normalized attribute */ function createStyleProperty(el) { var style = el.style; var output = {}; for (var i = 0; i < style.length; ++i) { var item = style.item(i); output[item] = String(style[item]); // hack to workaround browser inconsistency with url() if (output[item].indexOf('url') > -1) { output[item] = output[item].replace(/\"/g, '') } } return { name: 'style', value: output }; }