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