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