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

Source: selection-change.js

/**
 * selection-change.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 selection-change
 */
define([
	'functions',
	'arrays',
	'boundaries',
	'browsers',
	'events'
], function (
	Fn,
	Arrays,
	Boundaries,
	Browsers,
	Events
) {
	'use strict';

	/**
	 * Sometimes Firefox (tested with version 25.0) changes the selection only
	 * immediatly after a mouseup (both when the listener was registered with
	 * useCapture true and false). This seems to happen only in rare cases. One
	 * way to reproduce it is to have an editable like this (just a plain
	 * editable without Aloha):
	 *
	 * <div contenteditable="true">
	 *  xxxxxxxxxxxxxx<br/>
	 *  xxxxxxxxxxxxxxx<br type="_moz"/>
	 * </div>
	 *
	 * where the second line is written by just holding down the x key, and
	 * releasing it, and typing an individual x at the end of the line, and
	 * setting the selection with a mouse press after and before the last
	 * character. I think it has to do with the last character being an
	 * individual text node.
	 *
	 * The same goes for the keypress event (in both IE and Firefox and possibly
	 * others), except with the keypress event the selection seems to be never
	 * up to date, so we would always have to do it. Handling keypresses is
	 * useful to get a selection update when the user auto-repeats text-input by
	 * holding down a key. It's not a big deal however if on each keypress event
	 * the user gets the selection change caused by a previous keypress event,
	 * because the keyup event when the user releases the key will ensure a
	 * correct notification at the end of an auto-repeat sequence.
	 *
	 * Because Firefox sets the new selection immediately after the event
	 * handler returns we can use nextTick() instead of a timeout. This could
	 * lead to the handler passed to watchSelection() being called even after
	 * calling the freeing function returned by watchSelection().
	 *
	 * NB: keeping around events in a timeout in IE9 causes strange behaviour:
	 * the mouseup events are somehow played back again after they happened.
	 */
	function maybeNextTick(event, watchSelection) {
		var type = event.type;
		// Because the only browser where can confirm the problem is Firefox,
		// and doing it anyway may cause problems on IE.
		if (Browsers.mozilla && 'mouseup' === type) {
			Events.nextTick(Fn.partial(watchSelection, event));
		}
	}

	/**
	 * Creates a handler that can be used to listen to selection change events,
	 * and which will call the given handler function when the selection
	 * changes.
	 *
	 * See watchSelection().
	 *
	 * @param {function():Array.<Boundary>} getBoundaries
	 * @param {Array.<Boundary>} boundaries current selection
	 * @param fn {!function(Array.<Boundary>, Event)}
	 *        A handler function that will be called with the changed
	 *        selection, and the event that caused the selection change.
	 * @memberOf selection-change
	 */
	function handler(getBoundaries, boundaries, fn) {
		return function watchSelection(event) {
			var newBoundaries = getBoundaries();
			if (newBoundaries && !Arrays.equal(boundaries, newBoundaries, Boundaries.equals)) {
				boundaries = newBoundaries;
				fn(newBoundaries, event);
			} else {
				maybeNextTick(event, watchSelection);
			}
		};
	}

	/**
	 * Adds a handler function to events that may cause a selection-change.
	 *
	 * Our strategy
	 *
	 * Use the selectionchange event (see below) when available (Chrome, IE) and
	 * additionally hook into keyup, keypress, mouseup, touchend events (other
	 * browsers). We need keyup events even in IE to detect selection changes
	 * caused by text input. Keypress events are necessary to capture selection
	 * changes when the user is auto-repeating text-input by holding down a key,
	 * except in Chrome it's not necessary because there the selectionchange
	 * event fires on text-input (See maybeNextTick() for more information
	 * regarding auto-repeating text-input). Hooking into all events on all
	 * browsers does no harm. Touchend is probably necessary for mobile support
	 * other than webkit, although I only tested it on webkit, where it is not
	 * necessary due to selectionchange support.
	 *
	 * For programmatic selection changes we recommend programmatically firing
	 * the selectionchange event on the document element (IE7 needs the document
	 * element, but for IE9+, Chrome and Firefox triggering it on an element
	 * works too).
	 *
	 * We set useCapture to true, so that a stopPropagation call in the bubbling
	 * phase will not starve our handlers. In IE < 9 someone may still do it
	 * since useCapture is not supported.
	 *
	 * Behaviour of the 'selectionchange' event:
	 * * will be fired on every selection change, including when the user
	 *   selects something by pressing and holding the mouse button and
	 *   dragging the selection,
	 * * will not be fired when the user enters text e.g. in a content
	 *   editable in IE9 and IE10,
	 * * will be fired when the selection is set programatically in Chrome,
	 *   but not in IE9 and IE10,
	 * * works in IE as far back as IE7 and Chrome but doesn't work in Firefox or Opera.
	 * * can be feature detected with ('onselectionchange' in document).
	 *
	 * @param {!Document} doc
	 * @param {!function(Array.<Boundary>, Event)} watchSelection
	 *        A handler function like the one returned from handler().
	 * @param {boolean=} mousemove
	 *        Even with all the events above hooked, we only get up-to-date
	 *        selection change updates when the user presses the mouse and drags
	 *        the selection in Chrome and IE, but not in Firefox (and probably
	 *        others). This case can be covered by handling the mousemove event.
	 *        We don't do it by default because handling the mousemove event
	 *        could have different implications from handling up/down/press
	 *        events.
	 *
	 * @memberOf selection-change
	 */
	function addHandler(doc, watchSelection, mousemove) {
		// Chrome, IE (except IE text input)
		Events.add(doc, 'selectionchange', watchSelection, true);
		// IE and others
		Events.add(doc, 'keyup', watchSelection, true);
		// Others
		Events.add(doc, 'mouseup', watchSelection, true);
		Events.add(doc, 'touchend', watchSelection, true);
		Events.add(doc, 'keypress', watchSelection, true);
		// Because we know Chrome and IE behave acceptably we only do it for
		// Firefox and others.
		if (!Browsers.webkit && !Browsers.msie && mousemove) {
			Events.add(doc, 'mousemove', watchSelection, true);
		}
	}

	/**
	 * Removes a handler add with addHandler().
	 *
	 * All arguments including mousemove must be the same as when the handler
	 * was added.
	 *
	 * Expect the handler to be called even after it was removed.
	 *
	 * @param {!Document}                          doc
	 * @param {!function(Array.<Boundary>, Event)} watchSelection
	 * @param {boolean=}                           mousemove
	 * @memberOf selection-change
	 */
	function removeHandler(doc, watchSelection, mousemove) {
		Events.remove(doc, 'selectionchange', watchSelection, true);
		Events.remove(doc, 'keyup', watchSelection, true);
		Events.remove(doc, 'mouseup', watchSelection, true);
		Events.remove(doc, 'touchend', watchSelection, true);
		Events.remove(doc, 'keypress', watchSelection, true);
		if (!Browsers.webkit && !Browsers.msie && mousemove) {
			Events.remove(doc, 'mousemove', watchSelection, true);
		}
	}

	return {
		handler       : handler,
		addHandler    : addHandler,
		removeHandler : removeHandler
	};
});
comments powered by Disqus