UNPKG

can-view-nodelist

Version:
497 lines (467 loc) 15.5 kB
"use strict"; var namespace = require('can-namespace'); var domMutate = require('can-dom-mutate/node'); // # can/view/node_lists/node_list.js // // ### What's a nodeList? // // A nodelist is an array of DOM nodes (elements text nodes and DOM elements) and/or other // nodeLists, along with non-array-indexed properties that manage relationships between lists. // These properties are: // // * deepChildren children that couldn't be found by iterating over the nodeList when nesting // * nesting nested level of a nodelist (parent's nesting plus 1) // * newDeepChildren same as deepChildren but stored before registering with update() // * parentList the direct parent nodeList of this nodeList // * replacements an array of nodeLists meant to replace virtual nodes // * unregistered a callback to call when unregistering a nodeList // ## Helpers // A mapping of element ids to nodeList id allowing us to quickly find an element // that needs to be replaced when updated. var nodeMap = new Map(), splice = [].splice, push = [].push, // ## nodeLists.itemsInChildListTree // Given a nodeList return the number of child items in the provided // list and any child lists. itemsInChildListTree = function(list){ var count = 0; for(var i = 0, len = list.length ; i < len; i++){ var item = list[i]; // If the item is an HTMLElement then increment the count by 1. if(item.nodeType) { count++; } else { // If the item is not an HTMLElement it is a list, so // increment the count by the number of items in the child // list. count += itemsInChildListTree(item); } } return count; }, // replacements is an array of nodeLists // makes a map of the first node in the replacement to the nodeList replacementMap = function(replacements){ var map = new Map(); for(var i = 0, len = replacements.length; i < len; i++){ var node = nodeLists.first(replacements[i]); map.set(node, replacements[i]); } return map; }, addUnfoundAsDeepChildren = function(list, rMap){ rMap.forEach(function(replacement){ list.newDeepChildren.push(replacement); }); }; // ## Registering & Updating // // To keep all live-bound sections knowing which elements they are managing, // all live-bound elments are registered and updated when they change. // // For example, here's a template: // // <div> // {{#if items.length}} // Items: // {{#each items}} // <label>{{.}}</label> // {{/each}} // {{/if}} // </div> // // // the above template, when rendered with data like: // // data = new can.Map({ // items: ["first","second"] // }) // // This will first render the following content: // // <div> // <#text ""> // </div> // // The empty text node has a callback which, when called, will register it like: // // var ifsNodes = [<#text "">] // nodeLists.register(ifsNodes); // // And then render `{{if}}`'s contents and update `ifsNodes` with it: // // nodeLists.update( ifsNodes, [<#text "\nItems:\n">, <#text "">] ); // // Next, that final text node's callback is called which will regsiter it like: // // var eachsNodes = [<#text "">]; // nodeLists.register(eachsNodes); // // And then it will render `{{#each}}`'s content and update `eachsNodes` with it: // // nodeLists.update(eachsNodes, [<label>,<label>]); // // As `nodeLists` knows that `eachsNodes` is inside `ifsNodes`, it also updates // `ifsNodes`'s nodes to look like: // // [<#text "\nItems:\n">,<label>,<label>] // // Now, if all items were removed, `{{#if}}` would be able to remove // all the `<label>` elements. // // When you regsiter a nodeList, you can also provide a callback to know when // that nodeList has been replaced by a parent nodeList. This is // useful for tearing down live-binding. var nodeLists = { /** * @function can-view-nodelist.update update * @parent can-view-nodelist/methods * * @signature `nodeLists.update(nodeList, newNodes)` * * Updates a nodeList with new items, i.e. when values for the template have changed. * * @param {can-view-nodelist/types/NodeList} nodeList The list to update with the new nodes. * @param {can-view-nodelist/types/NodeList} newNodes The new nodes to update with. * * @return {Array<Node>} The nodes that were removed from `nodeList`. */ update: function (nodeList, newNodes, oldNodes) { // Unregister all childNodeLists. if(!oldNodes) { // if oldNodes has been passed, we assume everything has already been unregistered. oldNodes = nodeLists.unregisterChildren(nodeList); } var arr = []; for (var i = 0, ref = arr.length = newNodes.length; i < ref; i++) { arr[i] = newNodes[i]; } // see https://jsperf.com/nodelist-to-array newNodes = arr; var oldListLength = nodeList.length; // Replace oldNodeLists's contents. splice.apply(nodeList, [ 0, oldListLength ].concat(newNodes)); // Replacements are nodes that have replaced the original element this is on. // We can't simply insert elements because stache does children before parents. if(nodeList.replacements){ nodeLists.nestReplacements(nodeList); nodeList.deepChildren = nodeList.newDeepChildren; nodeList.newDeepChildren = []; } else { nodeLists.nestList(nodeList); } return oldNodes; }, /** * @function can-view-nodelist.nestReplacements nestReplacements * @parent can-view-nodelist/methods * @signature `nodeLists.nestReplacements(list)` * * Goes through each node in the list. `[el1, el2, el3, ...]` * Finds the nodeList for that node in replacements. el1's nodeList might look like `[el1, [el2]]`. * Replaces that element and any other elements in the node list with the * nodelist itself. resulting in `[ [el1, [el2]], el3, ...]` * If a replacement is not found, it was improperly added, so we add it as a deepChild. * * @param {can-view-nodelist/types/NodeList} list The nodeList of nodes to go over * */ nestReplacements: function(list){ var index = 0, // replacements are in reverse order in the DOM rMap = replacementMap(list.replacements), rCount = list.replacements.length; while(index < list.length && rCount) { var node = list[index], replacement = rMap.get(node); if( replacement ) { rMap["delete"](node); list.splice( index, itemsInChildListTree(replacement), replacement ); rCount--; } index++; } // Only do this if if(rCount) { addUnfoundAsDeepChildren(list, rMap ); } list.replacements = []; }, /** * @function can-view-nodelist.nestList nestList * @parent can-view-nodelist/methods * @signature `nodeLists.nestList(list)` * * If a given list does not exist in the nodeMap then create an lookup * id for it in the nodeMap and assign the list to it. * If the the provided does happen to exist in the nodeMap update the * elements in the list. * * @param {can-view-nodelist/types/NodeList} list The nodeList being nested. * */ nestList: function(list){ var index = 0; while(index < list.length) { var node = list[index], childNodeList = nodeMap.get(node); if(childNodeList) { // if this node is in another nodelist if(childNodeList !== list) { // update this nodeList to point to the childNodeList list.splice( index, itemsInChildListTree(childNodeList), childNodeList ); } } else { // Indicate the new nodes belong to this list. nodeMap.set(node, list); } index++; } }, /** * @function can-view-nodelist.last last * @parent can-view-nodelist/methods * @signature `nodeLists.last(nodeList)` * * Return the last HTMLElement in a nodeList; if the last * element is a nodeList, returns the last HTMLElement of * the child list, etc. * * @param {can-view-nodelist/types/NodeList} nodeList A nodeList. * @return {HTMLElement} The last element of the last list nested in this list. * */ last: function(nodeList){ var last = nodeList[nodeList.length - 1]; // If the last node in the list is not an HTMLElement // it is a nodeList so call `last` again. if(last.nodeType) { return last; } else { return nodeLists.last(last); } }, /** * @function can-view-nodelist.first first * @parent can-view-nodelist/methods * @signature `nodeLists.first(nodeList)` * * Return the first HTMLElement in a nodeList; if the first * element is a nodeList, returns the first HTMLElement of * the child list, etc. * * @param {can-view-nodelist/types/NodeList} nodeList A nodeList. * @return {HTMLElement} The first element of the first list nested in this list. * * */ first: function(nodeList) { var first = nodeList[0]; // If the first node in the list is not an HTMLElement // it is a nodeList so call `first` again. if(first.nodeType) { return first; } else { return nodeLists.first(first); } }, flatten: function(nodeList){ var items = []; for(var i = 0 ; i < nodeList.length; i++) { var item = nodeList[i]; if(item.nodeType) { items.push(item); } else { items.push.apply(items, nodeLists.flatten(item)); } } return items; }, /** * @function can-view-nodelist.register register * @parent can-view-nodelist/methods * * @signature `nodeLists.register(nodeList, unregistered, parent, directlyNested)` * * Registers a nodeList and returns the nodeList passed to register. * * @param {can-view-nodelist/types/NodeList} nodeList A nodeList. * @param {function()} unregistered A callback to call when the nodeList is unregistered. * @param {can-view-nodelist/types/NodeList} parent The parent nodeList of this nodeList. * @param {Boolean} directlyNested `true` if nodes in the nodeList are direct children of the parent. * @return {can-view-nodelist/types/NodeList} The passed in nodeList. * */ register: function (nodeList, unregistered, parent, directlyNested) { // If a unregistered callback has been provided assign it to the nodeList // as a property to be called when the nodeList is unregistred. nodeList.unregistered = unregistered; nodeList.parentList = parent; nodeList.nesting = parent && typeof parent.nesting !== 'undefined' ? parent.nesting + 1 : 0; if(parent) { nodeList.deepChildren = []; nodeList.newDeepChildren = []; nodeList.replacements = []; if(parent !== true) { if(directlyNested) { parent.replacements.push(nodeList); } else { parent.newDeepChildren.push(nodeList); } } } else { nodeLists.nestList(nodeList); } return nodeList; }, /** * @function can-view-nodelist.unregisterChildren unregisterChildren * @parent can-view-nodelist/methods * @signature `nodeLists.unregisterChildren(nodeList)` * * Unregister all childen within the provided list and return the * unregistred nodes. * * @param {can-view-nodelist/types/NodeList} nodeList The nodeList of child nodes to unregister. * @return {Array} The list of all nodes that were unregistered. */ unregisterChildren: function(nodeList){ var nodes = []; // For each node in the nodeList we want to compute it's id // and delete it from the nodeList's internal map. for (var n = 0; n < nodeList.length; n++) { var node = nodeList[n]; // If the node does not have a nodeType it is an array of // nodes. if(node.nodeType) { if(!nodeList.replacements) { nodeMap["delete"](node); } nodes.push(node); } else { // Recursively unregister each of the child lists in // the nodeList. push.apply(nodes, nodeLists.unregister(node, true)); } } var deepChildren = nodeList.deepChildren; if (deepChildren) { for (var l = 0; l < deepChildren.length; l++) { nodeLists.unregister(deepChildren[l], true); } } return nodes; }, /** @function can-view-nodelist.unregister unregister @parent can-view-nodelist/methods @signature `nodeLists.unregister(nodeList, isChild)` @param {ArrayLike} nodeList a nodeList to unregister from its parent @param {isChild} true if the nodeList is a direct child, false if a deep child @return {Array} a list of all nodes that were unregistered Unregister's a nodeList and returns the unregistered nodes. Call if the nodeList is no longer being updated. This will also unregister all child nodeLists. */ unregister: function (nodeList, isChild) { var nodes = nodeLists.unregisterChildren(nodeList, true); nodeList.isUnregistered = true; // If an 'unregisted' function was provided during registration, remove // it from the list, and call the function provided. if (nodeList.unregistered) { var unregisteredCallback = nodeList.unregistered; nodeList.replacements = nodeList.unregistered = null; if(!isChild) { var deepChildren = nodeList.parentList && nodeList.parentList.deepChildren; if(deepChildren) { var index = deepChildren.indexOf(nodeList); if(index !== -1) { deepChildren.splice(index,1); } } } unregisteredCallback(); } return nodes; }, /** * @function can-view-nodelist.after after * @parent can-view-nodelist/methods * @hide * @signature `nodeLists.after(oldElements, newFrag)` * * Inserts `newFrag` after `oldElements`. * * @param {ArrayLike<Node>} oldElements The elements to use as reference. * @param {DocumentFragment} newFrag The fragment to insert. * */ after: function (oldElements, newFrag) { var last = oldElements[oldElements.length - 1]; // Insert it in the `document` or `documentFragment` if (last.nextSibling) { domMutate.insertBefore.call(last.parentNode, newFrag, last.nextSibling); } else { domMutate.appendChild.call(last.parentNode, newFrag ); } }, /** * @function can-view-nodelist.replace replace * @hide * @parent can-view-nodelist/methods * @signature `nodeLists.replace(oldElements, newFrag)` * * Replaces `oldElements` with `newFrag`. * * @param {Array<Node>} oldElements the list elements to remove * @param {DocumentFragment} newFrag the fragment to replace the old elements * */ replace: function (oldElements, newFrag) { // The following helps make sure that a selected <option> remains // the same by removing `selected` from the currently selected option // and adding selected to an option that has the same value. var selectedValue, parentNode = oldElements[0].parentNode; if(parentNode.nodeName.toUpperCase() === "SELECT" && parentNode.selectedIndex >= 0) { selectedValue = parentNode.value; } if(oldElements.length === 1) { domMutate.replaceChild.call(parentNode, newFrag, oldElements[0]); } else { nodeLists.after(oldElements, newFrag); nodeLists.remove(oldElements); } if(selectedValue !== undefined) { parentNode.value = selectedValue; } }, /** * @function can-view-nodelist.remove remove * @parent can-view-nodelist/methods * @hide * @signature `nodeLists.remove(elementsToBeRemoved)` * * Remove all Nodes in `oldElements` from the DOM. * * @param {ArrayLike<Node>} oldElements the list of Elements to remove (must have a common parent) * */ remove: function(elementsToBeRemoved){ var parent = elementsToBeRemoved[0] && elementsToBeRemoved[0].parentNode; var child; for (var i = 0; i < elementsToBeRemoved.length; i++) { child = elementsToBeRemoved[i]; if(child.parentNode === parent) { domMutate.removeChild.call(parent, child); } } }, nodeMap: nodeMap }; module.exports = namespace.nodeLists = nodeLists;