/* ephemera.js is part of Aloha Editor project http://aloha-editor.org * * Aloha Editor is a WYSIWYG HTML5 inline editing library and editor. * Copyright (c) 2010-2012 Gentics Software GmbH, Vienna, Austria. * Contributors http://aloha-editor.org/contribution.php * * Aloha Editor is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or any later version. * * Aloha Editor is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * As an additional permission to the GNU GPL version 2, you may distribute * non-source (e.g., minimized or compacted) forms of the Aloha-Editor * source code without the copy of the GNU GPL normally required, * provided you include this license notice and a URL through which * recipients can access the Corresponding Source. */ define([ 'jquery', 'util/maps', 'util/strings', 'util/browser' ], function ( $, Maps, Strings, Browser ) { 'use strict'; var spacesRx = /\s+/; var attrRegex = /\s([^\/<>\s=]+)(?:=(?:"[^"]*"|'[^']*'|[^>\/\s]+))?/g; /** * Like insertBefore, inserts firstChild into parent before * refChild, except also inserts all the following siblings of * firstChild. */ function moveNextAll(parent, firstChild, refChild) { while (firstChild) { var nextChild = firstChild.nextSibling; parent.insertBefore(firstChild, refChild); firstChild = nextChild; } } /** * Used to serialize outerHTML of DOM elements in older (pre-HTML5) Gecko, * Safari, and Opera browsers. * * Beware that XMLSerializer generates an XHTML string (
* instead of ). It is noted here: * http://stackoverflow.com/questions/1700870/how-do-i-do-outerhtml-in-firefox * that some browsers (like older versions of Firefox) have problems with * XMLSerializer, and an alternative, albeit more expensive option, is * described. * * @type {XMLSerializer|null} */ var Serializer = window.XMLSerializer && new window.XMLSerializer(); /** * Gets the serialized HTML that describes the given DOM element and its * innerHTML. * * Polyfill for older versions of Gecko, Safari, and Opera browsers. * @see https://bugzilla.mozilla.org/show_bug.cgi?id=92264 for background. * * @param {HTMLElement} node DOM Element. * @return {String} */ function outerHtml(node) { var html = node.outerHTML; if (typeof html !== 'undefined') { return html; } try { return Serializer ? Serializer.serializeToString(node) : node.xml; } catch (e) { return node.xml; } } /** * Retrieves the names of all attributes from the given elmenet. * * Correctly handles the case that IE7 and IE8 have approx 70-90 * default attributes on each and every element. * * This implementation does not iterate over the elem.attributes * property since that is much slower on IE7 (even when * checking the attrNode.specified property). Instead it parses the * HTML of the element. For elements with few attributes the * performance on IE7 is improved by an order of magnitued. * * On IE7, when you clone a or an * element the boolean properties will * not be set on the cloned node. We choose the speed optimization * over correctness in this case. The dom-to-xhtml plugin has a * workaround for this case. */ function attrNames(elem) { var names = []; var html = outerHtml(elem.cloneNode(false)); var match; while (null != (match = attrRegex.exec(html))) { names.push(match[1]); } return names; } /** * Gets the attributes of the given element. * * See attrNames() for an edge case on IE7. * * @param elem * An element to get the attributes for. * @return * An array containing [name, value] tuples for each attribute. * Attribute values will always be strings, but possibly empty strings. */ function attrs(elem) { var as = []; var names = attrNames(elem); var i; var len; for (i = 0, len = names.length; i < len; i++) { var name = names[i]; var value = $.attr(elem, name); if (null == value) { value = ""; } else { value = value.toString(); } as.push([name, value]); } return as; } /** * Like indexByClass() but operates on a list of elements instead. * The given list may be a NodeList, HTMLCollection, or an array. */ function indexByClassHaveList(elems, classMap) { var index = {}, indexed, classes, elem, cls, len, i, j; for (i = 0, len = elems.length; i < len; i++) { elem = elems[i]; if (elem.className) { classes = Strings.words(elem.className); for (j = 0; j < classes.length; j++) { cls = classes[j]; if (classMap[cls]) { indexed = index[cls]; if (indexed) { indexed.push(elem); } else { index[cls] = [elem]; } } } } } return index; } /** * Indexes descendant elements based on the individual classes in * the class attribute. * * Based on these observations; * * * $('.class1, .class2') takes twice as long as $('.class1') on IE7. * * * $('.class1, .class2') is fast on IE8 (approx the same as * $('.class'), no matter how many classes), but if the individual * elements in the result set should be handled differently, the * subsequent hasClass('.class1') and hasClass('.class2') calls * slow things down again. * * * DOM traversal with elem.firstChild elem.nextSibling is very * slow on IE7 compared to just iterating over * root.getElementsByTagName('*'). * * * $('name.class') is much faster than just $('.class'), but as * soon as you need a single class in classMap that may be present * on any element, that optimization doesn't gain anything since * then you have to examine every element. * * This function will always take approx. the same amount of time * (on IE7 approx. equivalent to a single call to $('.class')) no * matter how many entries there are in classMap to index. * * This function only makes sense for multiple entries in * classMap. For a single class lookup, $('.class') or * $('name.class') is fine (even better in the latter case). * * @param root * The root element to search for elements to index * (will not be included in search). * @param classMap * A map from class name to boolean true. * @return * A map from class name to an array of elements with that class. * Every entry in classMap for which elements have been found * will have a corresponding entry in the returned * map. Entries for which no elements have been found, may or * may not have an entry in the returned map. */ function indexByClass(root, classMap) { var elems; if (Browser.ie7) { elems = root.getElementsByTagName('*'); } else { // Optimize for browsers that support querySelectorAll/getElementsByClassName. // On IE8 for example, if there is a relatively high // elems/resultSet ratio, performance can improve by a factor of 2. elems = $(root).find('.' + Maps.keys(classMap).join(',.')); } return indexByClassHaveList(elems, classMap); } /** * Indexes descendant elements based on elem.nodeName. * * Based on these observations: * * * On IE8, for moderate values of names.length, individual calls to * getElementsByTagName is just as fast as $root.find('name, name, * name, name'). * * * On IE7, $root.find('name, name, name, name') is extemely slow * (can be an order of magnitude slower than individual calls to * getElementsByTagName, why is that?). * * * Although getElementsByTagName is very fast even on IE7, when * names.length > 7 an alternative implementation that iterates * over all tags and checks names from a hashmap (similar to how * indexByClass does it) may become interesting, but * names.length > 7 is unlikely. * * This function only makes sense if the given names array has many * entries. For only one or two different names, calling $('name') * or context.getElementsByTagName(name) directly is fine (but * beware of $('name, name, ...') as explained above). * * The signature of this function differs from indexByClass by not * taking a map but instead an array of names. * * @param root * The root element to search for elements to index * (will not be included in search). * @param names * An array of element names to look for. * Names must be in all-uppercase (the same as elem.nodeName). * @return * A map from element name to an array of elements with that name. * Names will be all-uppercase. * Arrays will be proper arrays, not NodeLists. * Every entry in classMap for which elements have been found * will have a corresponding entry in the returned * map. Entries for which no elements have been found, may or * may not have an entry in the returned map. */ function indexByName(root, names) { var i, index = {}, len; for (i = 0, len = names.length; i < len; i++) { var name = names[i]; index[name] = $.makeArray(root.getElementsByTagName(name)); } return index; } return { moveNextAll: moveNextAll, attrNames: attrNames, attrs: attrs, indexByClass: indexByClass, indexByName: indexByName, indexByClassHaveList: indexByClassHaveList, outerHtml: outerHtml }; });