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