1 /* ephemera.js is part of Aloha Editor project http://aloha-editor.org 2 * 3 * Aloha Editor is a WYSIWYG HTML5 inline editing library and editor. 4 * Copyright (c) 2010-2012 Gentics Software GmbH, Vienna, Austria. 5 * Contributors http://aloha-editor.org/contribution.php 6 * 7 * Aloha Editor is free software; you can redistribute it and/or 8 * modify it under the terms of the GNU General Public License 9 * as published by the Free Software Foundation; either version 2 10 * of the License, or any later version. 11 * 12 * Aloha Editor is distributed in the hope that it will be useful, 13 * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 * GNU General Public License for more details. 16 * 17 * You should have received a copy of the GNU General Public License 18 * along with this program; if not, write to the Free Software 19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 20 * 21 * As an additional permission to the GNU GPL version 2, you may distribute 22 * non-source (e.g., minimized or compacted) forms of the Aloha-Editor 23 * source code without the copy of the GNU GPL normally required, 24 * provided you include this license notice and a URL through which 25 * recipients can access the Corresponding Source. 26 */ 27 /** 28 * Provides functions to mark the contents of editables as ephemeral. An 29 * editable's ephemeral content will be pruned before it is being 30 * returned by editable.getContents(). 31 * 32 * It is planned to replace most instances of makeClean() with this 33 * implementation for improved performance and more importantly, in 34 * order to have a centralized place that has the control over all 35 * ephemeral content, which can be leveraged by plugins to provide more 36 * advanced functionality. 37 * 38 * Some examples that would be possible: 39 * * a HTML source code text box, an interactive tree structure, or 40 * other kind of DOM visualization, next to the editable, that 41 * contains just the content of the editable (without ephemeral data) 42 * and which is updated efficiently in real time after each keystroke. 43 * 44 * * change detection algorithms that are able to intelligently ignore 45 * ephemeral data and which would not trigger unless non-ephemeral 46 * data is added to the editable. 47 * 48 * * When a plugin provides very general functionality over all nodes of 49 * the DOM, somtimes the plugin may not know what is and what isn't 50 * supposed to be real content. The functionality provided here makes 51 * it possible for the plugin to exaclty distinguish real content from 52 * ephemeral content. 53 * 54 * TODO: currently only simple transformations are suppored, like 55 * marking classes, attributes and elements as ephemeral and removing 56 * them during the pruning process. 57 * In the future, support for the block-plugin and custom pruning 58 * functions should be added. This may be done by letting implementations 59 * completely control the pruning of a DOM element through a 60 * function that takes the content+ephemeral-data and returns only 61 * content - similar to make clean, but for single elements to reduce 62 * overhead. 63 */ 64 define([ 65 'jquery', 66 'aloha/core', 67 'aloha/console', 68 'util/strings', 69 'util/trees', 70 'util/arrays', 71 'util/maps', 72 'util/dom2', 73 'util/functions', 74 'util/misc', 75 'PubSub' 76 ], function ( 77 $, 78 Aloha, 79 console, 80 Strings, 81 Trees, 82 Arrays, 83 Maps, 84 Dom, 85 Functions, 86 Misc, 87 PubSub 88 ) { 89 'use strict'; 90 91 var ephemeraMap = { 92 classMap: { 93 'aloha-cleanme': true, 94 'aloha-ui-wrapper': true, 95 'aloha-ui-filler': true, 96 'aloha-ui-attr': true 97 }, 98 attrMap: { 99 'hidefocus': true, 100 'hideFocus': true, 101 'tabindex': true, 102 'tabIndex': true, 103 'TABLE.contenteditable': true, 104 'TABLE.contentEditable': true 105 }, 106 attrRxs: [/^(?:nodeIndex|sizcache|sizset|jquery)[\w\d]*$/i], 107 pruneFns: [] 108 }; 109 110 var commonClsSubstr = 'aloha-'; 111 112 /** 113 * Checks whether the given classes contain the substring common to 114 * all ephemeral classes. If the check fails, an warning will be 115 * logged and the substring will be set to the empty string which 116 * voids the performance improvement the common substring would 117 * otherwise have gained. 118 */ 119 function checkCommonSubstr(clss) { 120 var i, len; 121 for (i = 0, len = clss.length; i < len; i++) { 122 if (-1 === clss[i].indexOf(commonClsSubstr)) { 123 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.'); 124 commonClsSubstr = ''; 125 } 126 } 127 } 128 129 /** 130 * Registers ephemeral classes. 131 * 132 * An ephemeral class is a non-content class that will be pruned 133 * from the from the result of editable.getContents(). 134 * 135 * The given classes should contain the string 'aloha-' to get the 136 * benefit of a performance optimization. 137 * 138 * Returns a map that contains all classes that were ever registered 139 * with this function. 140 * 141 * Multiple classes may be specified. If none are specified, just 142 * returns the current ephemeral classes map without modifying it. 143 * 144 * Also see ephemera(). 145 */ 146 function classes() { 147 var clss = Array.prototype.slice.call(arguments); 148 Maps.fillKeys(ephemeraMap.classMap, clss, true); 149 checkCommonSubstr(clss); 150 PubSub.pub('aloha.ephemera.classes', { 151 ephemera: ephemeraMap, 152 newClasses: clss 153 }); 154 } 155 156 /** 157 * Registers ephemeral attributes by attribute name. 158 * 159 * Similar to classes() except applies to entire attributes instead 160 * of individual classes in the class attribute. 161 */ 162 function attributes() { 163 var attrs = Array.prototype.slice.call(arguments); 164 Maps.fillKeys(ephemeraMap.attrMap, attrs, true); 165 PubSub.pub('aloha.ephemera.attributes', { 166 ephemera: ephemeraMap, 167 newAttributes: attrs 168 }); 169 } 170 171 /** 172 * Merges a map containing values to identify ephemeral content into 173 * a global registry. 174 * 175 * The given map may have the following entries 176 * classMap - a map from class name to the value true 177 * attrMap - a map from attribute name to the value true; attribute 178 * names may be optionally prefixed with "ELEMENT.", 179 * where ELEMENT is the name of an element in uppercase, 180 * to prune only from specific elements. An element name prefix 181 * should always be specified if it is known, and if 182 * multiple are known, multiple entries with separate 183 * element prefixes should be made instead of a single 184 * entry without - preserve information for refactoring. 185 * attrRxs - an array of regexes in object form (/[a-z].../ and not "[a-z]...") 186 * pruneFns - an array of functions that will be called at each pruning step. 187 * 188 * Returns the global registry, which has the same structure as above. 189 * 190 * When a DOM tree is pruned with prune(elem) without an emap 191 * argument, the global registry maintained with classes() 192 * attributes() and ephemera() is used as a default map. If an emap 193 * argument is specified, the global registry will be ignored and 194 * the emap argument will be used instead. 195 * 196 * When a DOM tree is pruned with prune() 197 * * classes specified by classMap will be removed 198 * * attributes specified by attrMap or attrRxs will be removed 199 * * functions specified by pruneFns will be called as the DOM tree 200 * is descended into (pre-order), with each node (element, text, 201 * etc.) as a single argument. The function is free to modify the 202 * element and return it, or return a new element which will 203 * replace the given element in the pruned tree. If null or 204 * undefined is returned, the element will be removed from the 205 * tree. As per contract of Maps.walkDomInplace, it is allowed to 206 * insert/remove children in the parent node as long as the given 207 * node is not removed. 208 * 209 * Also see classes() and attributes(). 210 * 211 * Note that removal of attributes doesn't always work on IE7 (in 212 * rare special cases). The dom-to-xhtml plugin can reliably remove 213 * ephemeral attributes during the serialization step. 214 */ 215 function ephemera(emap) { 216 if (emap) { 217 if (emap.classMap) { 218 $.extend(ephemeraMap.classMap, emap.classMap); 219 } 220 if (emap.attrMap) { 221 $.extend(ephemeraMap.attrMap, emap.attrMap); 222 } 223 if (emap.attrRxs) { 224 ephemeraMap.attrRxs = ephemeraMap.attrRxs.concat(emap.attrRxs); 225 } 226 if (emap.pruneFns) { 227 ephemeraMap.pruneFns = ephemeraMap.pruneFns.concat(emap.pruneFns); 228 } 229 PubSub.pub('aloha.ephemera', { 230 ephemera: ephemeraMap, 231 newEphemera: emap 232 }); 233 } 234 return ephemeraMap; 235 } 236 237 /** 238 * Marks an element as ephemeral. 239 * 240 * The element will be completely removed when the prune function is 241 * called on it. 242 */ 243 function markElement(elem) { 244 $(elem).addClass('aloha-cleanme'); 245 } 246 247 /** 248 * Marks the attribute of an element as ephemeral. 249 * 250 * The attribute will be removed from the element when the prune 251 * function is called on it. 252 * 253 * Multiple attributes can be passed at the same time be separating 254 * them with a space. 255 */ 256 function markAttribute(elem, attr) { 257 elem = $(elem); 258 var data = elem.attr('data-aloha-ui-attr'); 259 data = (null == data || '' === data ? attr : data + ' ' + attr); 260 elem.attr('data-aloha-ui-attr', data); 261 elem.addClass('aloha-ui-attr'); 262 } 263 264 /** 265 * Marks an element as a ephemeral, excluding subnodes. 266 * 267 * The element will be removed when the prune function is called on 268 * it, but any children of the wrapper element will remain in its 269 * place. 270 * 271 * A wrapper is an element that wraps a single non-ephemeral 272 * element. A filler is an element that is wrapped by a single 273 * non-ephemeral element. This distinction is not important for the 274 * prune function, which behave the same for both wrappers and 275 * fillers, but it makes it easier to build more advanced content 276 * inspection algorithms (also see note at the header of ephemeral.js). 277 * 278 * NB: a wrapper element must not wrap a filler element. Wrappers 279 * and fillers are ephermeral. A wrapper must always wrap a 280 * single _non-ephemeral_ element, and a filler must always fill 281 * a single _non-ephemeral_ element. 282 */ 283 function markWrapper(elem) { 284 $(elem).addClass('aloha-ui-wrapper'); 285 } 286 287 /** 288 * Marks an element as ephemeral, excluding subnodes. 289 * 290 * See wrapper() 291 */ 292 function markFiller(elem) { 293 $(elem).addClass('aloha-ui-filler'); 294 } 295 296 /** 297 * Prunes attributes marked as ephemeral with Ephemera.attributes() 298 * from the given element. 299 */ 300 function pruneMarkedAttrs(elem) { 301 var $elem = $(elem); 302 var data = $elem.attr('data-aloha-ui-attr'); 303 var i; 304 var attrs; 305 $elem.removeAttr('data-aloha-ui-attr'); 306 if (typeof data === 'string') { 307 attrs = Strings.words(data); 308 for (i = 0; i < attrs.length; i++) { 309 310 $elem.removeAttr(attrs[i]); 311 } 312 } 313 } 314 315 /** 316 * Determines whether the given attribute of the given element is 317 * ephemeral according to the given emap. 318 * See Ephemera.ephemera() for an explanation of attrMap and attrRxs. 319 */ 320 function isAttrEphemeral(elem, attrName, attrMap, attrRxs) { 321 return attrMap[attrName] || Misc.anyRx(attrRxs, attrName) || attrMap[elem.nodeName + '.' + attrName]; 322 } 323 324 /** 325 * Prunes attributes specified with either emap.attrMap or emap.attrRxs. 326 * See ephemera(). 327 */ 328 function pruneEmapAttrs(elem, emap) { 329 var $elem = null, 330 attrs = Dom.attrNames(elem), 331 name, 332 i, 333 len; 334 for (i = 0, len = attrs.length; i < len; i++) { 335 name = attrs[i]; 336 if (isAttrEphemeral(elem, name, emap.attrMap, emap.attrRxs)) { 337 $elem = $elem || $(elem); 338 $elem.removeAttr(name); 339 } 340 } 341 } 342 343 /** 344 * Prunes an element of attributes and classes or removes the 345 * element by returning false. 346 * 347 * Elements attributes and classes can either be marked as 348 * ephemeral, in which case the element itself will contain the 349 * prune-info, or they can be specified as ephemeral with the given 350 * emap. 351 * 352 * See ephemera() for an explanation of the emap argument. 353 */ 354 function pruneElem(elem, emap) { 355 var className = elem.className; 356 if (className && -1 !== className.indexOf(commonClsSubstr)) { 357 var classes = Strings.words(className); 358 359 // Ephemera.markElement() 360 if (-1 !== Arrays.indexOf(classes, 'aloha-cleanme')) { 361 $.removeData(elem); // avoids memory leak 362 return false; // removes the element 363 } 364 365 // Ephemera.markWrapper() and Ephemera.markFiller() 366 if (-1 !== Arrays.indexOf(classes, 'aloha-ui-wrapper') || -1 !== Arrays.indexOf(classes, 'aloha-ui-filler')) { 367 Dom.moveNextAll(elem.parentNode, elem.firstChild, elem.nextSibling); 368 $.removeData(elem); 369 return false; 370 } 371 372 373 // Ephemera.markAttribute() 374 if (-1 !== Arrays.indexOf(classes, 'aloha-ui-attr')) { 375 pruneMarkedAttrs(elem); 376 } 377 378 // Ephemera.classes() and Ehpemera.ephemera({ classMap: {} }) 379 var persistentClasses = Arrays.filter(classes, function (cls) { 380 return !emap.classMap[cls]; 381 }); 382 if (persistentClasses.length !== classes.length) { 383 if (0 === persistentClasses.length) { 384 // Removing the attributes is dangerous. Aloha has a 385 // jquery patch in place to fix some issue. 386 $(elem).removeAttr('class'); 387 } else { 388 elem.className = persistentClasses.join(' '); 389 } 390 } 391 } 392 393 // Ephemera.attributes() and Ephemera.ephemera({ attrMap: {}, attrRxs: {} }) 394 pruneEmapAttrs(elem, emap); 395 396 return true; 397 } 398 399 /** 400 * Called for each node during the pruning of a DOM tree. 401 402 */ 403 function pruneStep(emap, step, node) { 404 if (1 === node.nodeType) { 405 if (!pruneElem(node, emap)) { 406 return []; 407 } 408 node = Trees.walkDomInplace(node, step); 409 } 410 411 // Ephemera.ephemera({ pruneFns: [] }) 412 node = Arrays.reduce(emap.pruneFns, node, Arrays.applyNotNull); 413 if (!node) { 414 return []; 415 } 416 417 return [node]; 418 } 419 420 /** 421 * Prunes the given element of all ephemeral data. 422 * 423 * Elements marked with Ephemera.markElement() will be removed. 424 * Attributes marked with Ephemera.markAttribute() will be removed. 425 * Elements marked with Ephemera.markWrapper() or 426 * Ephemera.markFiller() will be replaced with their children. 427 * 428 * See ephemera() for an explanation of the emap argument. 429 * 430 * All properties of emap, if specified, are required, but may be 431 * empty. 432 * 433 * The element is modified in-place and returned. 434 */ 435 function prune(elem, emap) { 436 emap = emap || ephemeraMap; 437 438 function pruneStepClosure(node) { 439 return pruneStep(emap, pruneStepClosure, node); 440 } 441 return pruneStepClosure(elem)[0]; 442 } 443 444 if (Aloha.settings.ephemera) { 445 ephemera(Aloha.settings.ephemera); 446 } 447 448 return { 449 ephemera: ephemera, 450 classes: classes, 451 attributes: attributes, 452 markElement: markElement, 453 markAttribute: markAttribute, 454 markWrapper: markWrapper, 455 markFiller: markFiller, 456 prune: prune, 457 isAttrEphemeral: isAttrEphemeral 458 }; 459 }); 460