UNPKG

can-view-target

Version:

Fast cloning micro templates

293 lines (252 loc) 8.11 kB
"use strict"; /* jshint maxdepth:7 */ /* jshint latedef:false */ var getDocument = require('can-globals/document/document'); var domMutate = require('can-dom-mutate/node'); var namespace = require('can-namespace'); var MUTATION_OBSERVER = require('can-globals/mutation-observer/mutation-observer'); // if an object or a function // convert into what it should look like // then the modification can happen in place // but it has to have more than the current node // blah! var processNodes = function(nodes, paths, location, document){ var frag = document.createDocumentFragment(); for(var i = 0, len = nodes.length; i < len; i++) { var node = nodes[i]; frag.appendChild( processNode(node,paths,location.concat(i), document) ); } return frag; }, keepsTextNodes = typeof document !== "undefined" && (function(){ var testFrag = document.createDocumentFragment(); var div = document.createElement("div"); div.appendChild(document.createTextNode("")); div.appendChild(document.createTextNode("")); testFrag.appendChild(div); var cloned = testFrag.cloneNode(true); return cloned.firstChild.childNodes.length === 2; })(), clonesWork = typeof document !== "undefined" && (function(){ // Since html5shiv is required to support custom elements, assume cloning // works in any browser that doesn't have html5shiv // Clone an element containing a custom tag to see if the innerHTML is what we // expect it to be, or if not it probably was created outside of the document's // namespace. var el = document.createElement('a'); el.innerHTML = "<xyz></xyz>"; var clone = el.cloneNode(true); var works = clone.innerHTML === "<xyz></xyz>"; var MO, observer; if(works) { // Cloning text nodes with dashes seems to create multiple nodes in IE11 when // MutationObservers of subtree modifications are used on the documentElement. // Since this is not what we expect we have to include detecting it here as well. el = document.createDocumentFragment(); el.appendChild(document.createTextNode('foo-bar')); MO = MUTATION_OBSERVER(); if (MO) { observer = new MO(function() {}); observer.observe(document.documentElement, { childList: true, subtree: true }); clone = el.cloneNode(true); observer.disconnect(); } else { clone = el.cloneNode(true); } return clone.childNodes.length === 1; } return works; })(), namespacesWork = typeof document !== "undefined" && !!document.createElementNS; /** * @function cloneNode * @hide * * A custom cloneNode function to be used in browsers that properly support cloning * of custom tags (IE8 for example). Fixes it by doing some manual cloning that * uses innerHTML instead, which has been shimmed. * * @param {DocumentFragment} frag A document fragment to clone * @return {DocumentFragment} a new fragment that is a clone of the provided argument */ var cloneNode = clonesWork ? function(el){ return el.cloneNode(true); } : function(node){ var document = node.ownerDocument; var copy; if(node.nodeType === 1) { if(node.namespaceURI !== 'http://www.w3.org/1999/xhtml' && namespacesWork && document.createElementNS) { copy = document.createElementNS(node.namespaceURI, node.nodeName); } else { copy = document.createElement(node.nodeName); } } else if(node.nodeType === 3){ copy = document.createTextNode(node.nodeValue); } else if(node.nodeType === 8) { copy = document.createComment(node.nodeValue); } else if(node.nodeType === 11) { copy = document.createDocumentFragment(); } if(node.attributes) { var attributes = node.attributes; for (var i = 0; i < attributes.length; i++) { var attribute = attributes[i]; if (attribute && attribute.specified) { // If the attribute has a namespace set the namespace // otherwise it will be set to null if (attribute.namespaceURI) { copy.setAttributeNS(attribute.namespaceURI, attribute.nodeName || attribute.name, attribute.nodeValue || attribute.value); } else { copy.setAttribute(attribute.nodeName || attribute.name, attribute.nodeValue || attribute.value); } } } } if(node && node.firstChild) { var child = node.firstChild; while(child) { copy.appendChild( cloneNode(child) ); child = child.nextSibling; } } return copy; }; function processNode(node, paths, location, document){ var callback, loc = location, nodeType = typeof node, el, p, i , len; var getCallback = function(){ if(!callback) { callback = { path: location, callbacks: [] }; paths.push(callback); loc = []; } return callback; }; if(nodeType === "object") { if( node.tag ) { if(namespacesWork && node.namespace) { el = document.createElementNS(node.namespace, node.tag); } else { el = document.createElement(node.tag); } if(node.attrs) { for(var attrName in node.attrs) { var value = node.attrs[attrName]; if(typeof value === "function"){ getCallback().callbacks.push({ callback: value }); } else if (value !== null && typeof value === "object" && value.namespaceURI) { el.setAttributeNS(value.namespaceURI,attrName,value.value); } else { domMutate.setAttribute.call(el, attrName, value); } } } if(node.attributes) { for(i = 0, len = node.attributes.length; i < len; i++ ) { getCallback().callbacks.push({callback: node.attributes[i]}); } } if(node.children && node.children.length) { // add paths if(callback) { p = callback.paths = []; } else { p = paths; } el.appendChild( processNodes(node.children, p, loc, document) ); } } else if(node.comment) { el = document.createComment(node.comment); if(node.callbacks) { for(i = 0, len = node.callbacks.length; i < len; i++ ) { getCallback().callbacks.push({callback: node.callbacks[i]}); } } } } else if(nodeType === "string"){ el = document.createTextNode(node); } else if(nodeType === "function") { if(keepsTextNodes) { el = document.createTextNode(""); getCallback().callbacks.push({ callback: node }); } else { el = document.createComment("~"); getCallback().callbacks.push({ callback: function(){ var el = document.createTextNode(""); domMutate.replaceChild.call(this.parentNode, el, this); return node.apply(el,arguments ); } }); } } return el; } function getCallbacks(el, pathData, elementCallbacks){ var path = pathData.path, callbacks = pathData.callbacks, paths = pathData.paths, child = el, pathLength = path ? path.length : 0, pathsLength = paths ? paths.length : 0; for(var i = 0; i < pathLength; i++) { child = child.childNodes.item(path[i]); } for( i= 0 ; i < pathsLength; i++) { getCallbacks(child, paths[i], elementCallbacks); } elementCallbacks.push({element: child, callbacks: callbacks}); } function hydrateCallbacks(callbacks, args) { var len = callbacks.length, callbacksLength, callbackElement, callbackData; for(var i = 0; i < len; i++) { callbackData = callbacks[i]; callbacksLength = callbackData.callbacks.length; callbackElement = callbackData.element; for(var c = 0; c < callbacksLength; c++) { callbackData.callbacks[c].callback.apply(callbackElement, args); } } } function makeTarget(nodes, doc){ var paths = []; var frag = processNodes(nodes, paths, [], doc || getDocument()); return { paths: paths, clone: frag, hydrate: function(){ var cloned = cloneNode(this.clone); var args = []; for (var a = 0, ref = args.length = arguments.length; a < ref; a++) { args[a] = arguments[a]; } // see https://jsperf.com/nodelist-to-array var callbacks = []; for(var i = 0; i < paths.length; i++) { getCallbacks(cloned, paths[i], callbacks); } hydrateCallbacks(callbacks, args); return cloned; } }; } makeTarget.keepsTextNodes = keepsTextNodes; makeTarget.cloneNode = cloneNode; namespace.view = namespace.view || {}; module.exports = namespace.view.target = makeTarget;