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 'util/browser', 76 'PubSub' 77 ], function ( 78 $, 79 Aloha, 80 console, 81 Strings, 82 Trees, 83 Arrays, 84 Maps, 85 Dom, 86 Functions, 87 Misc, 88 Browser, 89 PubSub 90 ) { 91 'use strict'; 92 93 var ephemeraMap = { 94 classMap: { 95 'aloha-ephemera-wrapper': true, 96 'aloha-ephemera-filler': true, 97 'aloha-ephemera-attr': true, 98 'aloha-ephemera': true, 99 // aloha-cleanme is the same as aloha-ephemera. 100 // TODO: should be replaced with aloha-ephemera throughout 101 // the codebase and removed here. 102 'aloha-cleanme': true 103 }, 104 attrMap: { 105 'hidefocus': true, 106 'hideFocus': true, 107 'tabindex': true, 108 'tabIndex': true, 109 'contenteditable': ['TABLE'], 110 'contentEditable': ['TABLE'] 111 }, 112 attrRxs: [/^(?:nodeIndex|sizcache|sizset|jquery)[\w\d]*$/i], 113 pruneFns: [] 114 }; 115 116 var commonClsSubstr = 'aloha-'; 117 118 /** 119 * Checks whether the given classes contain the substring common to 120 * all ephemeral classes. If the check fails, an warning will be 121 * logged and the substring will be set to the empty string which 122 * voids the performance improvement the common substring would 123 * otherwise have gained. 124 */ 125 function checkCommonSubstr(clss) { 126 var i, len; 127 for (i = 0, len = clss.length; i < len; i++) { 128 if (-1 === clss[i].indexOf(commonClsSubstr)) { 129 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.'); 130 commonClsSubstr = ''; 131 } 132 } 133 } 134 135 /** 136 * Registers ephemeral classes. 137 * 138 * An ephemeral class is a non-content class that will be pruned 139 * from the from the result of editable.getContents(). 140 * 141 * The given classes should contain the string 'aloha-' to get the 142 * benefit of a performance optimization. 143 * 144 * Returns a map that contains all classes that were ever registered 145 * with this function. 146 * 147 * Multiple classes may be specified. If none are specified, just 148 * returns the current ephemeral classes map without modifying it. 149 * 150 * Also see ephemera(). 151 */ 152 function classes() { 153 var clss = Array.prototype.slice.call(arguments); 154 Maps.fillKeys(ephemeraMap.classMap, clss, true); 155 checkCommonSubstr(clss); 156 PubSub.pub('aloha.ephemera.classes', { 157 ephemera: ephemeraMap, 158 newClasses: clss 159 }); 160 } 161 162 /** 163 * Registers ephemeral attributes by attribute name. 164 * 165 * Similar to classes() except applies to entire attributes instead 166 * of individual classes in the class attribute. 167 */ 168 function attributes() { 169 var attrs = Array.prototype.slice.call(arguments); 170 Maps.fillKeys(ephemeraMap.attrMap, attrs, true); 171 PubSub.pub('aloha.ephemera.attributes', { 172 ephemera: ephemeraMap, 173 newAttributes: attrs 174 }); 175 } 176 177 /** 178 * Provides access to the global ephemera registry. 179 * 180 * If the given argument is not null, sets the global ephemera 181 * registry to the given value and returns it. Otherwise, just 182 * returns the global registry. 183 * 184 * The given/returned value has the following properties: 185 * 186 * The given map may have the following entries 187 * 188 * classMap - a map from class name to the value true. 189 * all classes must have a "aloha-" prefix. 190 * Use Ehpemera.attributes() to set classes without "aloha-" prefix. 191 * 192 * attrMap - a map from attribute name to the value true or to an array 193 * of element names. If an array of elements is specified, the 194 * attribute will only be considered ephemeral if it is 195 * found on an element in the array. 196 * 197 * attrRxs - an array of regexes (in object - not string - form: /[a-z].../) 198 * 199 * pruneFns - an array of functions that will be called at each pruning step. 200 * 201 * When a DOM tree is pruned with prune(elem) without an emap 202 * argument, the global registry maintained with classes() 203 * attributes() and ephemera() is used as a default map. If an emap 204 * argument is specified, the global registry will be ignored and 205 * the emap argument will be used instead. 206 * 207 * When a DOM tree is pruned with prune() 208 * - classes specified by classMap will be removed 209 * - attributes specified by attrMap or attrRxs will be removed 210 * - functions specified by pruneFns will be called as the DOM tree 211 * is descended into (pre-order), with each node (element, text, 212 * etc.) as a single argument. The function is free to modify the 213 * element and return it, or return a new element which will 214 * replace the given element in the pruned tree. If null or 215 * undefined is returned, the element will be removed from the 216 * tree. As per contract of Maps.walkDomInplace, it is allowed to 217 * insert/remove children in the parent node as long as the given 218 * node is not removed. 219 * 220 * Also see classes() and attributes(). 221 * 222 * Note that removal of attributes doesn't always work on IE7 (in 223 * rare special cases). The dom-to-xhtml plugin can reliably remove 224 * ephemeral attributes during the serialization step. 225 */ 226 function ephemera(emap) { 227 if (emap) { 228 ephemeraMap = emap; 229 } 230 PubSub.pub('aloha.ephemera', { 231 ephemera: ephemeraMap 232 }); 233 return ephemeraMap; 234 } 235 236 /** 237 * Marks an element as ephemeral. 238 * 239 * The element will be completely removed when the prune function is 240 * called on it. 241 * 242 * Adds the class 'aloha-ephemera' to the given element. 243 * 244 * The class 'aloha-ephemera' can also be added directly without 245 * recurse to this function, if that is more convenient. 246 */ 247 function markElement(elem) { 248 $(elem).addClass('aloha-ephemera'); 249 } 250 251 /** 252 * Marks the attribute of an element as ephemeral. 253 254 * 255 * The attribute will be removed from the element when the prune 256 * function is called on it. 257 * 258 * Multiple attributes can be passed at the same time be separating 259 * them with a space. 260 * 261 * Adds the class 'aloha-ephemera-attr' to the given element. Also 262 * adds or modifies the 'data-aloha-ephemera-attr' attribute, 263 * and adds to it the name of the given attribute. 264 * 265 * These modifications can be made directly without recurse to this 266 * function, if that is more convenient. 267 */ 268 function markAttr(elem, attr) { 269 elem = $(elem); 270 var data = elem.attr('data-aloha-ephemera-attr'); 271 if (null == data || '' === data) { 272 data = attr; 273 } else if (-1 === Arrays.indexOf(Strings.words(data), attr)) { 274 data += ' ' + attr; 275 } 276 elem.attr('data-aloha-ephemera-attr', data); 277 elem.addClass('aloha-ephemera-attr'); 278 } 279 280 /** 281 * Marks an element as a ephemeral, excluding subnodes. 282 * 283 * The element will be removed when the prune function is called on 284 * it, but any children of the wrapper element will remain in its 285 * place. 286 * 287 * A wrapper is an element that wraps a single non-ephemeral 288 * element. A filler is an element that is wrapped by a single 289 * non-ephemeral element. This distinction is not important for the 290 * prune function, which behave the same for both wrappers and 291 * fillers, but it makes it easier to build more advanced content 292 * inspection algorithms (also see note at the header of ephemeral.js). 293 * 294 * Adds the class 'aloha-ephemera-wrapper' to the given element. 295 * 296 * The class 'aloha-ephemera-wrapper' may also be added directly, 297 * without recurse to this function, if that is more convenient. 298 * 299 * NB: a wrapper element must not wrap a filler element. Wrappers 300 * and fillers are ephermeral. A wrapper must always wrap a 301 * single _non-ephemeral_ element, and a filler must always fill 302 * a single _non-ephemeral_ element. 303 */ 304 function markWrapper(elem) { 305 $(elem).addClass('aloha-ephemera-wrapper'); 306 } 307 308 /** 309 * Marks an element as ephemeral, excluding subnodes. 310 * 311 * Adds the class 'aloha-ephemera-filler' to the given element. 312 * 313 * The class 'aloha-ephemera-filler' may also be added directly, 314 * without recurse to this function, if that is more convenient. 315 * 316 * See wrapper() 317 */ 318 function markFiller(elem) { 319 $(elem).addClass('aloha-ephemera-filler'); 320 } 321 322 /** 323 * Prunes attributes marked as ephemeral with Ephemera.attributes() 324 * from the given element. 325 */ 326 function pruneMarkedAttrs(elem) { 327 var $elem = $(elem); 328 var data = $elem.attr('data-aloha-ephemera-attr'); 329 var i; 330 var attrs; 331 // Because IE7 crashes if we remove this attribute. If the 332 // dom-to-xhtml plugin is turned on, it will handle the removal 333 // of this attribute during serialization. 334 if (!Browser.ie7) { 335 $elem.removeAttr('data-aloha-ephemera-attr'); 336 } 337 if (typeof data === 'string') { 338 attrs = Strings.words(data); 339 for (i = 0; i < attrs.length; i++) { 340 $elem.removeAttr(attrs[i]); 341 } 342 } 343 } 344 345 /** 346 * Determines whether the given attribute of the given element is 347 * ephemeral according to the given emap. 348 * See Ephemera.ephemera() for an explanation of attrMap and attrRxs. 349 */ 350 function isAttrEphemeral(elem, attrName, attrMap, attrRxs) { 351 var mapped = attrMap[attrName]; 352 if (mapped) { 353 // The attrMap may either contain boolean true or an array of element names. 354 if (true === mapped) { 355 return true; 356 } 357 if (-1 !== Arrays.indexOf(mapped, elem.nodeName)) { 358 return true; 359 } 360 } 361 return Misc.anyRx(attrRxs, attrName); 362 } 363 364 /** 365 * Prunes attributes specified with either emap.attrMap or emap.attrRxs. 366 * See ephemera(). 367 */ 368 function pruneEmapAttrs(elem, emap) { 369 var $elem = null, 370 attrs = Dom.attrNames(elem), 371 name, 372 i, 373 len; 374 for (i = 0, len = attrs.length; i < len; i++) { 375 name = attrs[i]; 376 if (isAttrEphemeral(elem, name, emap.attrMap, emap.attrRxs)) { 377 $elem = $elem || $(elem); 378 $elem.removeAttr(name); 379 380 } 381 } 382 } 383 384 /** 385 * Prunes an element of attributes and classes or removes the 386 * element by returning false. 387 * 388 * Elements attributes and classes can either be marked as 389 * ephemeral, in which case the element itself will contain the 390 * prune-info, or they can be specified as ephemeral with the given 391 * emap. 392 * 393 * See ephemera() for an explanation of the emap argument. 394 */ 395 function pruneElem(elem, emap) { 396 var className = elem.className; 397 if (className && -1 !== className.indexOf(commonClsSubstr)) { 398 var classes = Strings.words(className); 399 400 // Ephemera.markElement() 401 if (-1 !== Arrays.indexOf(classes, 'aloha-cleanme') || -1 !== Arrays.indexOf(classes, 'aloha-ephemera')) { 402 $.removeData(elem); // avoids memory leak 403 return false; // removes the element 404 } 405 406 // Ephemera.markWrapper() and Ephemera.markFiller() 407 if (-1 !== Arrays.indexOf(classes, 'aloha-ephemera-wrapper') || -1 !== Arrays.indexOf(classes, 'aloha-ephemera-filler')) { 408 Dom.moveNextAll(elem.parentNode, elem.firstChild, elem.nextSibling); 409 $.removeData(elem); 410 return false; 411 } 412 413 414 // Ephemera.markAttr() 415 if (-1 !== Arrays.indexOf(classes, 'aloha-ephemera-attr')) { 416 pruneMarkedAttrs(elem); 417 } 418 419 // Ephemera.classes() and Ehpemera.ephemera({ classMap: {} }) 420 var persistentClasses = Arrays.filter(classes, function (cls) { 421 return !emap.classMap[cls]; 422 }); 423 if (persistentClasses.length !== classes.length) { 424 if (0 === persistentClasses.length) { 425 // Removing the attributes is dangerous. Aloha has a 426 // jquery patch in place to fix some issue. 427 $(elem).removeAttr('class'); 428 } else { 429 elem.className = persistentClasses.join(' '); 430 } 431 } 432 } 433 434 // Ephemera.attributes() and Ephemera.ephemera({ attrMap: {}, attrRxs: {} }) 435 pruneEmapAttrs(elem, emap); 436 437 return true; 438 } 439 440 /** 441 * Called for each node during the pruning of a DOM tree. 442 */ 443 function pruneStep(emap, step, node) { 444 if (1 === node.nodeType) { 445 if (!pruneElem(node, emap)) { 446 return []; 447 } 448 node = Trees.walkDomInplace(node, step); 449 } 450 451 // Ephemera.ephemera({ pruneFns: [] }) 452 node = Arrays.reduce(emap.pruneFns, node, Arrays.applyNotNull); 453 if (!node) { 454 return []; 455 } 456 457 return [node]; 458 } 459 460 /** 461 * Prunes the given element of all ephemeral data. 462 * 463 * Elements marked with Ephemera.markElement() will be removed. 464 * Attributes marked with Ephemera.markAttr() will be removed. 465 * Elements marked with Ephemera.markWrapper() or 466 * Ephemera.markFiller() will be replaced with their children. 467 * 468 * See ephemera() for an explanation of the emap argument. 469 * 470 * All properties of emap, if specified, are required, but may be 471 * empty. 472 * 473 * The element is modified in-place and returned. 474 */ 475 function prune(elem, emap) { 476 emap = emap || ephemeraMap; 477 478 function pruneStepClosure(node) { 479 return pruneStep(emap, pruneStepClosure, node); 480 } 481 return pruneStepClosure(elem)[0]; 482 } 483 484 return { 485 ephemera: ephemera, 486 classes: classes, 487 attributes: attributes, 488 markElement: markElement, 489 markAttr: markAttr, 490 markWrapper: markWrapper, 491 markFiller: markFiller, 492 prune: prune, 493 isAttrEphemeral: isAttrEphemeral 494 }; 495 }); 496