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