We use cookies to make our website more effective. By using our website you agree to our privacy policy.

Source: typing.js

/**
 * typing.js is part of Aloha Editor project http://www.alohaeditor.org
 *
 * Aloha Editor ● JavaScript Content Editing Library
 * Copyright (c) 2010-2015 Gentics Software GmbH, Vienna, Austria.
 * Contributors http://www.alohaeditor.org/docs/contributing.html
 * @namespace typing
 */
define([
	'dom',
	'keys',
	'html',
	'undo',
	'lists',
	'events',
	'arrays',
	'editing',
	'strings',
	'metaview',
	'mutation',
	'selections',
	'traversing',
	'boundaries',
	'overrides',
	'functions'
], function (
	Dom,
	Keys,
	Html,
	Undo,
	Lists,
	Events,
	Arrays,
	Editing,
	Strings,
	Metaview,
	Mutation,
	Selections,
	Traversing,
	Boundaries,
	Overrides,
	Fn
) {
	'use strict';

	function undoable(type, event, fn) {
		var range = Boundaries.range(
			event.selection.boundaries[0],
			event.selection.boundaries[1]
		);
		Undo.capture(event.editable.undoContext, {
			meta: {type: type},
			oldRange: range
		}, function () {
			range = fn();
			return {newRange: range};
		});
	}

	/**
	 * Removes unrendered containers from each of the given boundaries while
	 * preserving the correct position of all.
	 *
	 * Returns a new set of boundaries that represent the corrected positions
	 * following node-removal. The order of the returned list corresponds with
	 * the list of boundaries that was given.
	 *
	 * @private
	 * @param  {Array.<Boundary>} boundaries
	 * @return {Array.<Boundary>}
	 */
	function removeUnrenderedContainers(boundaries) {
		function remove(node) {
			boundaries = Mutation.removeNode(node, boundaries);
		}
		function isRendered(node) {
			return Html.isRendered(node) || Dom.isEditingHost(node);
		}
		for (var i = 0; i < boundaries.length; i++) {
			Dom.climbUntil(Boundaries.container(boundaries[i]), remove, isRendered);
		}
		return boundaries;
	}

	function remove(direction, event) {
		var selection = event.selection;
		var start = selection.boundaries[0];
		var end = selection.boundaries[1];
		if (Boundaries.equals(start, end)) {
			if (direction) {
				end = Traversing.next(end);
			} else {
				start = Traversing.prev(start);
			}
		}
		var boundaries = Editing.remove(
			start,
			Traversing.envelopeInvisibleCharacters(end)
		);
		selection.formatting = Overrides.joinToSet(
			selection.formatting,
			Overrides.harvest(Boundaries.container(boundaries[0]))
		);
		boundaries = removeUnrenderedContainers(boundaries);
		Html.prop(Boundaries.commonContainer(boundaries[0], boundaries[1]));
		return boundaries;
	}

	function format(style, event) {
		var selection = event.selection;
		var boundaries = selection.boundaries;
		if (!Html.isBoundariesEqual(boundaries[0], boundaries[1])) {
			return Editing.toggle(boundaries[0], boundaries[1], style);
		}
		var override = Overrides.nodeToState[style];
		if (!override) {
			return boundaries;
		}
		var overrides = Overrides.joinToSet(
			selection.formatting,
			Overrides.harvest(Boundaries.container(boundaries[0])),
			selection.overrides
		);
		selection.overrides = Overrides.toggle(overrides, override, true);
		return selection.boundaries;
	}

	function breakline(isLinebreak, event) {
		if (!isLinebreak) {
			event.selection.formatting = Overrides.joinToSet(
				event.selection.formatting,
				Overrides.harvest(Boundaries.container(event.selection.boundaries[0]))
			);
		}
		var breaker = (event.meta.indexOf('shift') > -1)
		            ? 'BR'
		            : event.editable.settings.defaultBlock;
		return Editing.breakline(event.selection.boundaries[1], breaker);
	}

	/**
	 * Checks whether the given normalized boundary is immediately behind of a
	 * whitespace character.
	 *
	 * @private
	 * @param  {!Boundary} boundary
	 * @return {boolean}
	 */
	function isBehindWhitespace(boundary) {
		var text = Boundaries.container(boundary).data;
		var offset = Boundaries.offset(boundary);
		return Strings.WHITE_SPACE.test(text.substr(offset - 1, 1));
	}

	/**
	 * Checks whether the given normalized boundary is immediately in front of a
	 * whitespace character.
	 *
	 * @private
	 * @param  {!Boundary} boundary
	 * @return {boolean}
	 */
	function isInfrontWhitespace(boundary) {
		var text = Boundaries.container(boundary).data;
		var offset = Boundaries.offset(boundary);
		return Strings.WHITE_SPACE.test(text.substr(offset, 1));
	}

	/**
	 * Given a normalized boundary, determines the appropriate white-space that
	 * should be inserted at the given normalized boundary position.
	 *
	 * Strategy:
	 *
	 * A non-breaking-white-space (0x00a0) is required if none of the two
	 * conditions below is met:
	 *
	 * Condition 1.
	 *
	 * The boundary is inside a text node with non-space characters adjacent on
	 * either side. This is often the case when seperating words with a space.
	 *
	 *
	 * Condition 2.
	 *
	 * From the boundary it is possible to locate a preceeding text node (using
	 * pre-order-backtracing traversal Dom.backwardPreorderBacktracingUntil)
	 * whose last character is a non-space character. This text node must be
	 * located before encountering a linebreaking element.
	 *
	 * ... and ...
	 *
	 * From the boundary it is possible to locate a preceeding text node (using
	 * pre-order-backtracing traversal. Dom.forwardPreorderBacktracingUntil)
	 * whose first character is a non-space character. This text node must be
	 * located before encountering a linebreaking element.
	 *
	 * @private
	 * @param  {!Boundary} boundary
	 * @return {string}
	 */
	function appropriateWhitespace(boundary) {
		if (Boundaries.isTextBoundary(boundary)) {
			return (isBehindWhitespace(boundary) || isInfrontWhitespace(boundary))
			     ? '\xa0'
			     : ' ';
		}
		var node = Boundaries.container(boundary);
		var stop = Dom.backwardPreorderBacktraceUntil(node, function (node) {
			return Dom.isTextNode(node)
			    || Dom.isEditingHost(node)
			    || Html.hasLinebreakingStyle(node);
		});
		if (Dom.isElementNode(stop)) {
			return '\xa0';
		}
		if (isBehindWhitespace(Boundaries.fromEndOfNode(stop))) {
			return '\xa0';
		}
		stop = Dom.forwardPreorderBacktraceUntil(node, function (node) {
			return Dom.isTextNode(node)
			    || Dom.isEditingHost(node)
			    || Html.hasLinebreakingStyle(node);
		});
		if (Dom.isElementNode(stop)) {
			return '\xa0';
		}
		if (isInfrontWhitespace(Boundaries.fromStartOfNode(stop))) {
			return '\xa0';
		}
		return ' ';
	}

	/**
	 * Looks to see if the boundary is immediately in front of a non-breaking
	 * whitespace and replaces it with a regular whitespace.
	 *
	 * It is necessary to do this normalization every time we insert any
	 * character that is not a whitespace character in order to not end up with
	 * a situation where all spaces between words being non-breaking
	 * whitespaces. Such a situation would otherwise arise because, when space
	 * inserting spaces at the end of a block-level element, these spaces need
	 * to be non-breaking whitespace for them to be visible.  But when a
	 * non-space character is inserted, that non-breaking whitespace is no
	 * longer needed and should converted to a collapsable whitespace.
	 *
	 * @private
	 * @param  {!Boundary} boundary
	 */
	function normalizePreceedingWhitespace(boundary) {
		var node, offset;
		if (Boundaries.isTextBoundary(boundary)) {
			node = Boundaries.container(boundary);
			offset = Boundaries.offset(boundary);
		} else {
			node = Boundaries.nodeBefore(boundary);
			if (node && Dom.isTextNode(node)) {
				offset = node.data.length;
			}
		}
		if (!node) {
			return;
		}
		var text = node.data;
		if (text && Strings.NON_BREAKING_SPACE.test(text.substr(offset - 1, 1))) {
			node.data = text.substr(0, offset - 1) + ' ' + text.substr(offset);
		}
	}

	function indent(event) {
		var boundaries = event.selection.boundaries;
		var start = boundaries[0];
		var end = boundaries[1];
		if (Lists.isIndentationRange(start, end)) {
			return Lists.indent(start, end);
		}
		if (!Boundaries.equals(start, end)) {
			event.selection.boundaries = remove(false, event);
		}
		return insertText(event);
	}

	function insertText(event) {
		var editable = event.editable;
		var selection = event.selection;
		var text = String.fromCharCode(event.keycode);
		var boundary = selection.boundaries[0];
		if ('\t' === text) {
			text = '\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0';
		} else if (' ' === text) {
			var whiteSpaceStyle = Dom.getComputedStyle(
				Dom.upWhile(Boundaries.container(boundary), Dom.isTextNode),
				'white-space'
			);
			if (!Html.isWhiteSpacePreserveStyle(whiteSpaceStyle)) {
				text = appropriateWhitespace(boundary);
			}
		} else {
			normalizePreceedingWhitespace(boundary);
		}
		boundary = Overrides.consume(boundary, Overrides.joinToSet(
			selection.formatting,
			selection.overrides
		));
		selection.overrides = [];
		selection.formatting = [];
		var range = Boundaries.range(boundary, boundary);
		var insertPath = Undo.pathFromBoundary(editable.elem, boundary);
		var insertContent = [editable.elem.ownerDocument.createTextNode(text)];
		var change = Undo.makeInsertChange(insertPath, insertContent);
		Undo.capture(editable.undoContext, {noObserve: true}, function () {
			Mutation.insertTextAtBoundary(text, boundary, true, [range]);
			return {changes: [change]};
		});
		return Boundaries.fromRange(range);
	}

	function toggleUndo(op, event) {
		var range = Boundaries.range(
			event.selection.boundaries[0],
			event.selection.boundaries[1]
		);
		op(event.editable.undoContext, range, [range]);
		return Boundaries.fromRange(range);
	}

	function selectEditable(event) {
		var editable = Dom.editingHost(Boundaries.commonContainer(
			event.selection.boundaries[0],
			event.selection.boundaries[1]
		));
		return !editable ? event.selection.boundaries : [
			Boundaries.create(editable, 0),
			Boundaries.fromEndOfNode(editable)
		];
	}

	/**
	 * Whether or not the given event represents a text input.
	 *
	 * @see
	 * https://lists.webkit.org/pipermail/webkit-dev/2007-December/002992.html
	 *
	 * @private
	 * @param  {AlohaEvent} event
	 * @return {boolean}
	 */
	function isTextInput(event) {
		return 'keypress' === event.type
		    && 'alt' !== event.meta
			&& 'ctrl' !== event.meta
		    && !Strings.isControlCharacter(String.fromCharCode(event.keycode));
	}

	var deleteBackward = {
		clearOverrides : true,
		preventDefault : true,
		undo           : 'delete',
		mutate         : Fn.partial(remove, false)
	};

	var deleteForward = {
		clearOverrides : true,
		preventDefault : true,
		undo           : 'delete',
		mutate         : Fn.partial(remove, true)
	};

	var breakBlock = {
		removeContent  : true,
		preventDefault : true,
		undo           : 'enter',
		mutate         : Fn.partial(breakline, false)
	};

	var breakLine = {
		removeContent  : true,
		preventDefault : true,
		undo           : 'enter',
		mutate         : Fn.partial(breakline, true)
	};

	var formatBold = {
		preventDefault : true,
		undo           : 'bold',
		mutate         : Fn.partial(format, 'B')
	};

	var formatItalic = {
		preventDefault : true,
		undo           : 'italic',
		mutate         : Fn.partial(format, 'I')
	};

	var formatUnderline = {
		preventDefault : true,
		undo           : 'underline',
		mutate         : Fn.partial(format, 'U')
	};

	var inputText = {
		removeContent  : true,
		preventDefault : true,
		undo           : 'typing',
		mutate         : insertText
	};

	var indentContent = {
		preventDefault : true,
		undo           : 'indent',
		mutate         : indent
	};

	var selectAll = {
		preventDefault : true,
		clearOverrides : true,
		mutate         : selectEditable
	};

	var undo = {
		clearOverrides : true,
		preventDefault : true,
		mutate         : Fn.partial(toggleUndo, Undo.undo)
	};

	var redo = {
		preventDefault : true,
		clearOverrides : true,
		mutate         : Fn.partial(toggleUndo, Undo.redo)
	};

	/**
	 * This variable is missing documentation.
	 * @TODO Complete documentation.
	 *
	 * @memberOf typing
	 */
	var actions = {
		'breakBlock'     : breakBlock,
		'breakLine'      : breakLine,
		'deleteBackward' : deleteBackward,
		'deleteForward'  : deleteForward,
		'formatBold'     : formatBold,
		'formatItalic'   : formatItalic,
		'inputText'      : inputText,
		'redo'           : redo,
		'undo'           : undo
	};

	var handlers = {
		'keydown'  : {},
		'keypress' : {},
		'keyup'    : {}
	};

	handlers['keydown']['up'] =
	handlers['keydown']['down'] =
	handlers['keydown']['left'] =
	handlers['keydown']['right'] = {clearOverrides: true};

	handlers['keydown']['delete'] = deleteForward;
	handlers['keydown']['backspace'] = deleteBackward;
	handlers['keydown']['enter'] = breakBlock;
	handlers['keydown']['shift+enter'] = breakLine;
	handlers['keydown']['ctrl+b'] =
	handlers['keydown']['meta+b'] = formatBold;
	handlers['keydown']['ctrl+i'] =
	handlers['keydown']['meta+i'] = formatItalic;
	handlers['keydown']['ctrl+u'] =
	handlers['keydown']['meta+u'] = formatUnderline;
	handlers['keydown']['ctrl+a'] =
	handlers['keydown']['meta+a'] = selectAll;
	handlers['keydown']['ctrl+z'] =
	handlers['keydown']['meta+z'] = undo;
	handlers['keydown']['ctrl+shift+z'] =
	handlers['keydown']['meta+shift+z'] = redo;
	handlers['keydown']['tab'] = indentContent;

	handlers['keypress']['input'] = inputText;

	handlers['keydown']['ctrl+0'] = {mutate : function toggleUndo(event) {
		if (event.editable) {
			Metaview.toggle(event.editable.elem);
		}
		return event.selection.boundaries;
	}};

	handlers['keydown']['ctrl+1'] = {mutate : function toggleUndo(event) {
		if (event.editable) {
			Metaview.toggle(event.editable.elem, {
				'outline': true,
				'tagname': true
			});
		}
		return event.selection.boundaries;
	}};

	handlers['keydown']['ctrl+2'] = {mutate : function toggleUndo(event) {
		if (event.editable) {
			Metaview.toggle(event.editable.elem, {
				'outline': true,
				'tagname': true,
				'padding': true
			});
		}
		return event.selection.boundaries;
	}};

	function handler(event) {
		return Keys.shortcutHandler(event.meta, event.keycode, handlers[event.type] || [])
		    || (isTextInput(event) && handlers['keypress']['input']);
	}

	/**
	 * Updates selection and nativeEvent
	 * @memberOf typing
	 */
	function middleware(event) {
		var selection = event.selection;
		var start = selection.boundaries[0];
		var end = selection.boundaries[1];
		var handling = handler(event);
		if (!handling) {
			return event;
		}
		if (handling.preventDefault) {
			Events.preventDefault(event.nativeEvent);
		}
		if (handling.clearOverrides) {
			selection.overrides = [];
			selection.formatting = [];
		}
		if (handling.mutate) {
			if (handling.undo) {
				undoable(handling.undo, event, function () {
					if (handling.removeContent && !Boundaries.equals(start, end)) {
						selection.boundaries = remove(false, event);
					}
					selection.boundaries = handling.mutate(event);
					Html.prop(Boundaries.commonContainer(
						selection.boundaries[0],
						selection.boundaries[1]
					));
				});
			} else {
				selection.boundaries = handling.mutate(event);
			}
		}
		return event;
	}

	return {
		middleware : middleware,
		actions    : actions
	};
});
comments powered by Disqus