/* 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. */ /** * Provides functions to mark the contents of editables as ephemeral. An * editable's ephemeral content will be pruned before it is being * returned by editable.getContents(). * * It is planned to replace most instances of makeClean() with this * implementation for improved performance and more importantly, in * order to have a centralized place that has the control over all * ephemeral content, which can be leveraged by plugins to provide more * advanced functionality. * * Some examples that would be possible: * * a HTML source code text box, an interactive tree structure, or * other kind of DOM visualization, next to the editable, that * contains just the content of the editable (without ephemeral data) * and which is updated efficiently in real time after each keystroke. * * * change detection algorithms that are able to intelligently ignore * ephemeral data and which would not trigger unless non-ephemeral * data is added to the editable. * * * When a plugin provides very general functionality over all nodes of * the DOM, somtimes the plugin may not know what is and what isn't * supposed to be real content. The functionality provided here makes * it possible for the plugin to exaclty distinguish real content from * ephemeral content. * * TODO: currently only simple transformations are suppored, like * marking classes, attributes and elements as ephemeral and removing * them during the pruning process. * In the future, support for the block-plugin and custom pruning * functions should be added. This may be done by letting implementations * completely control the pruning of a DOM element through a * function that takes the content+ephemeral-data and returns only * content - similar to make clean, but for single elements to reduce * overhead. */ define([ 'jquery', 'aloha/core', 'aloha/console', 'util/strings', 'util/trees', 'util/arrays', 'util/maps', 'util/dom2', 'util/functions', 'util/misc', 'util/browser', 'PubSub' ], function ( $, Aloha, console, Strings, Trees, Arrays, Maps, Dom, Functions, Misc, Browser, PubSub ) { 'use strict'; var ephemeraMap = { classMap: { 'aloha-ephemera-wrapper': true, 'aloha-ephemera-filler': true, 'aloha-ephemera-attr': true, 'aloha-ephemera': true, // aloha-cleanme is the same as aloha-ephemera. // TODO: should be replaced with aloha-ephemera throughout // the codebase and removed here. 'aloha-cleanme': true }, attrMap: { 'hidefocus': true, 'hideFocus': true, 'tabindex': true, 'tabIndex': true, 'contenteditable': ['TABLE'], 'contentEditable': ['TABLE'] }, attrRxs: [/^(?:nodeIndex|sizcache|sizset|jquery)[\w\d]*$/i], pruneFns: [] }; var commonClsSubstr = 'aloha-'; /** * Checks whether the given classes contain the substring common to * all ephemeral classes. If the check fails, an warning will be * logged and the substring will be set to the empty string which * voids the performance improvement the common substring would * otherwise have gained. */ function checkCommonSubstr(clss) { var i, len; for (i = 0, len = clss.length; i < len; i++) { if (-1 === clss[i].indexOf(commonClsSubstr)) { console.warn('Class "' + clss[i] + '" was set to be ephemeral,' + 'which hurts peformance.' + ' Add the common substring "' + commonClsSubstr + '" to the class to fix this problem.'); commonClsSubstr = ''; } } } /** * Registers ephemeral classes. * * An ephemeral class is a non-content class that will be pruned * from the from the result of editable.getContents(). * * The given classes should contain the string 'aloha-' to get the * benefit of a performance optimization. * * Returns a map that contains all classes that were ever registered * with this function. * * Multiple classes may be specified. If none are specified, just * returns the current ephemeral classes map without modifying it. * * Also see ephemera(). */ function classes() { var clss = Array.prototype.slice.call(arguments); Maps.fillKeys(ephemeraMap.classMap, clss, true); checkCommonSubstr(clss); PubSub.pub('aloha.ephemera.classes', { ephemera: ephemeraMap, newClasses: clss }); } /** * Registers ephemeral attributes by attribute name. * * Similar to classes() except applies to entire attributes instead * of individual classes in the class attribute. */ function attributes() { var attrs = Array.prototype.slice.call(arguments); Maps.fillKeys(ephemeraMap.attrMap, attrs, true); PubSub.pub('aloha.ephemera.attributes', { ephemera: ephemeraMap, newAttributes: attrs }); } /** * Provides access to the global ephemera registry. * * If the given argument is not null, sets the global ephemera * registry to the given value and returns it. Otherwise, just * returns the global registry. * * The given/returned value has the following properties: * * The given map may have the following entries * * classMap - a map from class name to the value true. * all classes must have a "aloha-" prefix. * Use Ehpemera.attributes() to set classes without "aloha-" prefix. * * attrMap - a map from attribute name to the value true or to an array * of element names. If an array of elements is specified, the * attribute will only be considered ephemeral if it is * found on an element in the array. * * attrRxs - an array of regexes (in object - not string - form: /[a-z].../) * * pruneFns - an array of functions that will be called at each pruning step. * * When a DOM tree is pruned with prune(elem) without an emap * argument, the global registry maintained with classes() * attributes() and ephemera() is used as a default map. If an emap * argument is specified, the global registry will be ignored and * the emap argument will be used instead. * * When a DOM tree is pruned with prune() * - classes specified by classMap will be removed * - attributes specified by attrMap or attrRxs will be removed * - functions specified by pruneFns will be called as the DOM tree * is descended into (pre-order), with each node (element, text, * etc.) as a single argument. The function is free to modify the * element and return it, or return a new element which will * replace the given element in the pruned tree. If null or * undefined is returned, the element will be removed from the * tree. As per contract of Maps.walkDomInplace, it is allowed to * insert/remove children in the parent node as long as the given * node is not removed. * * Also see classes() and attributes(). * * Note that removal of attributes doesn't always work on IE7 (in * rare special cases). The dom-to-xhtml plugin can reliably remove * ephemeral attributes during the serialization step. */ function ephemera(emap) { if (emap) { ephemeraMap = emap; } PubSub.pub('aloha.ephemera', { ephemera: ephemeraMap }); return ephemeraMap; } /** * Marks an element as ephemeral. * * The element will be completely removed when the prune function is * called on it. * * Adds the class 'aloha-ephemera' to the given element. * * The class 'aloha-ephemera' can also be added directly without * recurse to this function, if that is more convenient. */ function markElement(elem) { $(elem).addClass('aloha-ephemera'); } /** * Marks the attribute of an element as ephemeral. * * The attribute will be removed from the element when the prune * function is called on it. * * Multiple attributes can be passed at the same time be separating * them with a space. * * Adds the class 'aloha-ephemera-attr' to the given element. Also * adds or modifies the 'data-aloha-ephemera-attr' attribute, * and adds to it the name of the given attribute. * * These modifications can be made directly without recurse to this * function, if that is more convenient. */ function markAttr(elem, attr) { elem = $(elem); var data = elem.attr('data-aloha-ephemera-attr'); if (null == data || '' === data) { data = attr; } else if (-1 === Arrays.indexOf(Strings.words(data), attr)) { data += ' ' + attr; } elem.attr('data-aloha-ephemera-attr', data); elem.addClass('aloha-ephemera-attr'); } /** * Marks an element as a ephemeral, excluding subnodes. * * The element will be removed when the prune function is called on * it, but any children of the wrapper element will remain in its * place. * * A wrapper is an element that wraps a single non-ephemeral * element. A filler is an element that is wrapped by a single * non-ephemeral element. This distinction is not important for the * prune function, which behave the same for both wrappers and * fillers, but it makes it easier to build more advanced content * inspection algorithms (also see note at the header of ephemeral.js). * * Adds the class 'aloha-ephemera-wrapper' to the given element. * * The class 'aloha-ephemera-wrapper' may also be added directly, * without recurse to this function, if that is more convenient. * * NB: a wrapper element must not wrap a filler element. Wrappers * and fillers are ephermeral. A wrapper must always wrap a * single _non-ephemeral_ element, and a filler must always fill * a single _non-ephemeral_ element. */ function markWrapper(elem) { $(elem).addClass('aloha-ephemera-wrapper'); } /** * Marks an element as ephemeral, excluding subnodes. * * Adds the class 'aloha-ephemera-filler' to the given element. * * The class 'aloha-ephemera-filler' may also be added directly, * without recurse to this function, if that is more convenient. * * See wrapper() */ function markFiller(elem) { $(elem).addClass('aloha-ephemera-filler'); } /** * Prunes attributes marked as ephemeral with Ephemera.attributes() * from the given element. */ function pruneMarkedAttrs(elem) { var $elem = $(elem); var data = $elem.attr('data-aloha-ephemera-attr'); var i; var attrs; // Because IE7 crashes if we remove this attribute. If the // dom-to-xhtml plugin is turned on, it will handle the removal // of this attribute during serialization. if (!Browser.ie7) { $elem.removeAttr('data-aloha-ephemera-attr'); } if (typeof data === 'string') { attrs = Strings.words(data); for (i = 0; i < attrs.length; i++) { $elem.removeAttr(attrs[i]); } } } /** * Determines whether the given attribute of the given element is * ephemeral according to the given emap. * See Ephemera.ephemera() for an explanation of attrMap and attrRxs. */ function isAttrEphemeral(elem, attrName, attrMap, attrRxs) { var mapped = attrMap[attrName]; if (mapped) { // The attrMap may either contain boolean true or an array of element names. if (true === mapped) { return true; } if (-1 !== Arrays.indexOf(mapped, elem.nodeName)) { return true; } } return Misc.anyRx(attrRxs, attrName); } /** * Prunes attributes specified with either emap.attrMap or emap.attrRxs. * See ephemera(). */ function pruneEmapAttrs(elem, emap) { var $elem = null, attrs = Dom.attrNames(elem), name, i, len; for (i = 0, len = attrs.length; i < len; i++) { name = attrs[i]; if (isAttrEphemeral(elem, name, emap.attrMap, emap.attrRxs)) { $elem = $elem || $(elem); $elem.removeAttr(name); } } } /** * Prunes an element of attributes and classes or removes the * element by returning false. * * Elements attributes and classes can either be marked as * ephemeral, in which case the element itself will contain the * prune-info, or they can be specified as ephemeral with the given * emap. * * See ephemera() for an explanation of the emap argument. */ function pruneElem(elem, emap) { var className = elem.className; if (className && -1 !== className.indexOf(commonClsSubstr)) { var classes = Strings.words(className); // Ephemera.markElement() if (-1 !== Arrays.indexOf(classes, 'aloha-cleanme') || -1 !== Arrays.indexOf(classes, 'aloha-ephemera')) { $.removeData(elem); // avoids memory leak return false; // removes the element } // Ephemera.markWrapper() and Ephemera.markFiller() if (-1 !== Arrays.indexOf(classes, 'aloha-ephemera-wrapper') || -1 !== Arrays.indexOf(classes, 'aloha-ephemera-filler')) { Dom.moveNextAll(elem.parentNode, elem.firstChild, elem.nextSibling); $.removeData(elem); return false; } // Ephemera.markAttr() if (-1 !== Arrays.indexOf(classes, 'aloha-ephemera-attr')) { pruneMarkedAttrs(elem); } // Ephemera.classes() and Ehpemera.ephemera({ classMap: {} }) var persistentClasses = Arrays.filter(classes, function (cls) { return !emap.classMap[cls]; }); if (persistentClasses.length !== classes.length) { if (0 === persistentClasses.length) { // Removing the attributes is dangerous. Aloha has a // jquery patch in place to fix some issue. $(elem).removeAttr('class'); } else { elem.className = persistentClasses.join(' '); } } } // Ephemera.attributes() and Ephemera.ephemera({ attrMap: {}, attrRxs: {} }) pruneEmapAttrs(elem, emap); return true; } /** * Called for each node during the pruning of a DOM tree. */ function pruneStep(emap, step, node) { if (1 === node.nodeType) { if (!pruneElem(node, emap)) { return []; } node = Trees.walkDomInplace(node, step); } // Ephemera.ephemera({ pruneFns: [] }) node = Arrays.reduce(emap.pruneFns, node, Arrays.applyNotNull); if (!node) { return []; } return [node]; } /** * Prunes the given element of all ephemeral data. * * Elements marked with Ephemera.markElement() will be removed. * Attributes marked with Ephemera.markAttr() will be removed. * Elements marked with Ephemera.markWrapper() or * Ephemera.markFiller() will be replaced with their children. * * See ephemera() for an explanation of the emap argument. * * All properties of emap, if specified, are required, but may be * empty. * * The element is modified in-place and returned. */ function prune(elem, emap) { emap = emap || ephemeraMap; function pruneStepClosure(node) { return pruneStep(emap, pruneStepClosure, node); } return pruneStepClosure(elem)[0]; } return { ephemera: ephemera, classes: classes, attributes: attributes, markElement: markElement, markAttr: markAttr, markWrapper: markWrapper, markFiller: markFiller, prune: prune, isAttrEphemeral: isAttrEphemeral }; });