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