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