Source: delite/typematic.js

/**
 * These functions are used to repetitively call a user specified callback
 * method when a specific key or mouse click over a specific DOM node is
 * held down for a specific amount of time.
 * Only one such event is allowed to occur on the browser page at one time.
 * @module delite/typematic
 */
define([], function () {

	// TODO: do we still need this module at all?

	// TODO: wouldn't this be easier to use as a widget base class (although we would need to make sure the function
	// names wouldn't likely conflict with other general method names)?

	var typematic = /** @lends module:delite/typematic */ {
		_fireEventAndReload: function () {
			this._timer = null;
			this._callback(++this._count, this._node, this._evt);

			// Schedule next event, timer is at most minDelay (default 10ms) to avoid
			// browser overload (particularly avoiding starving DOH robot so it never gets to send a mouseup)
			this._currentTimeout = Math.max(
				this._currentTimeout < 0 ? this._initialDelay :
					(this._subsequentDelay > 1 ? this._subsequentDelay :
						Math.round(this._currentTimeout * this._subsequentDelay)),
				this._minDelay);
			this._timer = setTimeout(this._fireEventAndReload.bind(this), this._currentTimeout);
		},

		/**
		 * Callback function passed to addKeyListener(), addMouseListener(), and addListener().
		 * @callback module:delite/typematic.TriggerCallback
		 * @param {number} count - Integer representing number of repeated calls (0..n)
		 * with -1 indicating the iteration has stopped.
		 * @param {Element} node - The DOM node object passed in.
		 * @param {Event} evt - Key or mouse event object.
		 */

		/**
		 * Start a timed, repeating callback sequence.
		 * If already started, the function call is ignored.
		 * This method is not normally called by the user but can be
		 * when the normal listener code is insufficient.
		 * @param {Event} evt - Key or mouse event object to pass to the user callback.
		 * @param {module:delite/Widget} _this - Pointer to the user's widget space.
		 * @param {Element} node - The DOM node object to pass the the callback function.
		 * @param {module:delite/typematic.TriggerCallback} callback - Function to call until the sequence is stopped.
		 * @param {Object} obj - User space object used to uniquely identify each typematic sequence.
		 * @param {number} [subsequentDelay] - If > 1, the number of milliseconds until the 3->n events occur
		 * or else the fractional time multiplier for the next event's delay, default=0.9.
		 * @param {number} [initialDelay] - The number of milliseconds until the 2nd event occurs, default=500ms.
		 * @param {number} [minDelay] - The maximum delay in milliseconds for event to fire, default=10ms.
		 */
		trigger: function (evt, _this, node, callback, obj,
				subsequentDelay, initialDelay, minDelay) {
			if (obj !== this._obj) {
				this.stop();
				this._initialDelay = initialDelay || 500;
				this._subsequentDelay = subsequentDelay || 0.90;
				this._minDelay = minDelay || 10;
				this._obj = obj;
				this._node = node;
				this._currentTimeout = -1;
				this._count = -1;
				this._callback = callback.bind(_this);
				this._evt = { faux: true };
				for (var attr in evt) {
					if (attr !== "layerX" && attr !== "layerY") { // prevent WebKit warnings
						var v = evt[attr];
						if (typeof v !== "function" && typeof v !== "undefined") {
							this._evt[attr] = v;
						}
					}
				}
				this._fireEventAndReload();
			}
		},

		/**
		 * Stop an ongoing timed, repeating callback sequence.
		 */
		stop: function () {
			if (this._timer) {
				clearTimeout(this._timer);
				this._timer = null;
			}
			if (this._obj) {
				this._callback(-1, this._node, this._evt);
				this._obj = null;
			}
		},

		/**
		 * Handle to cancel a listener.
		 * @typedef module:delite/typematic.Handle
		 * @property {Function} remove - cancel the listener
		 */

		/**
		 * Start listening for a specific typematic key.
		 * See also the trigger method for other parameters.
		 * @param {Element} node
		 * @param {Object} keyObject - An object defining the key to listen for:
		 * - keyCode: the keyCode (number) to listen for, used for non-printable keys
		 * - charCode: the charCode (number) to listen for, used for printable keys
		 * - ctrlKey: desired ctrl key state to initiate the callback sequence:
		 * - pressed (true)
		 * - released (false)
		 * - either (unspecified)
		 * - altKey: same as ctrlKey but for the alt key
		 * - shiftKey: same as ctrlKey but for the shift key
		 * @param {module:delite/Widget} _this - Pointer to the user's widget space.
		 * @param {module:delite/typematic.TriggerCallback} callback - Function to call until the sequence is stopped.
		 * @param {number} [subsequentDelay] - If > 1, the number of milliseconds until the 3->n events occur
		 * or else the fractional time multiplier for the next event's delay, default=0.9.
		 * @param {number} [initialDelay] - The number of milliseconds until the 2nd event occurs, default=500ms.
		 * @param {number} [minDelay] - The maximum delay in milliseconds for event to fire, default=10ms.
		 * @returns {module:delite/typematic.Handle} A connection handle.
		 */
		addKeyListener: function (node, keyObject, _this, callback,
				subsequentDelay, initialDelay, minDelay) {
			// Setup keydown or keypress listener depending on whether keyCode or charCode was specified.
			var type = "keyCode" in keyObject ? "keydown" : "keypress",
				attr = "keyCode" in keyObject ? "keyCode" : "charCode";

			var handles = [
				_this.on(type, function (evt) {
					if (evt[attr] === keyObject[attr] &&
						(keyObject.ctrlKey === undefined || keyObject.ctrlKey === evt.ctrlKey) &&
						(keyObject.altKey === undefined || keyObject.altKey === evt.altKey) &&
						(keyObject.metaKey === undefined || keyObject.metaKey === evt.metaKey) &&
						(keyObject.shiftKey === undefined || keyObject.shiftKey === evt.shiftKey)) {
						evt.stopPropagation();
						evt.preventDefault();
						typematic.trigger(evt, _this, node, callback, keyObject,
							subsequentDelay, initialDelay, minDelay);
					} else if (typematic._obj === keyObject) {
						typematic.stop();
					}
				}, node),
				_this.on("keyup", function () {
					if (typematic._obj === keyObject) {
						typematic.stop();
					}
				}, node)
			];
			return {
				remove: function () {
					handles.forEach(function (h) {
						h.remove();
					});
				}
			};
		},

		/**
		 * Start listening for a typematic mouse click.
		 * See also the trigger method for other parameters.
		 * @param {Element} node
		 * @param {Object} keyObject - An object defining the key to listen for:
		 * - keyCode: the keyCode (number) to listen for, used for non-printable keys
		 * - charCode: the charCode (number) to listen for, used for printable keys
		 * - ctrlKey: desired ctrl key state to initiate the callback sequence:
		 * - pressed (true)
		 * - released (false)
		 * - either (unspecified)
		 * - altKey: same as ctrlKey but for the alt key
		 * - shiftKey: same as ctrlKey but for the shift key
		 * @param {module:delite/Widget} _this - Pointer to the user's widget space.
		 * @param {module:delite/typematic.TriggerCallback} callback - Function to call untilthe sequence is stopped.
		 * @param {number} [subsequentDelay] - If > 1, the number of milliseconds until the 3->n events occur
		 * or else the fractional time multiplier for the next event's delay, default=0.9.
		 * @param {number} [initialDelay] - The number of milliseconds until the 2nd event occurs, default=500ms.
		 * @param {number} [minDelay] - The maximum delay in milliseconds for event to fire, default=10ms.
		 * @returns {module:delite/typematic.Handle} A connection handle.
		 */
		addMouseListener: function (node,  _this, callback,
				subsequentDelay, initialDelay, minDelay) {
			var handles = [
				_this.on("mousedown", function (evt) {
					evt.preventDefault();
					typematic.trigger(evt, _this, node, callback, node, subsequentDelay, initialDelay, minDelay);
				}, node),
				_this.on("mouseup", function (evt) {
					if (this._obj) {
						evt.preventDefault();
					}
					typematic.stop();
				}.bind(this), node),
				_this.on("mouseout", function (evt) {
					if (this._obj) {
						evt.preventDefault();
					}
					typematic.stop();
				}.bind(this), node),
				_this.on("dblclick", function (evt) {
					evt.preventDefault();
				}, node)
			];
			return { remove: function () {
				handles.forEach(function (h) {
					h.remove();
				});
			} };
		},

		/**
		 * Start listening for a specific typematic key and mouseclick.
		 * This is a thin wrapper to addKeyListener and addMouseListener.
		 * @param {Element} mouseNode - The DOM node object to listen on for mouse events.
		 * @param {Element} keyNodeNode - The DOM node object to listen on for key events.
		 * @param {Object} keyObject - An object defining the key to listen for:
		 * - keyCode: the keyCode (number) to listen for, used for non-printable keys
		 * - charCode: the charCode (number) to listen for, used for printable keys
		 * - ctrlKey: desired ctrl key state to initiate the callback sequence:
		 * - pressed (true)
		 * - released (false)
		 * - either (unspecified)
		 * - altKey: same as ctrlKey but for the alt key
		 * - shiftKey: same as ctrlKey but for the shift key
		 * @param {module:delite/Widget} _this - Pointer to the user's widget space.
		 * @param {module:delite/typematic.TriggerCallback} callback - Function to call until the sequence is stopped.
		 * @param {number} [subsequentDelay] - If > 1, the number of milliseconds until the 3->n events occur
		 * or else the fractional time multiplier for the next event's delay, default=0.9.
		 * @param {number} [initialDelay] - The number of milliseconds until the 2nd event occurs, default=500ms.
		 * @param {number} [minDelay] - The maximum delay in milliseconds for event to fire, default=10ms.
		 * @returns {module:delite/typematic.Handle} A connection handle.
		 */
		addListener: function (mouseNode, keyNode, keyObject, _this,
				callback, subsequentDelay, initialDelay, minDelay) {
			var handles = [
				this.addKeyListener(keyNode, keyObject, _this, callback, subsequentDelay, initialDelay, minDelay),
				this.addMouseListener(mouseNode, _this, callback, subsequentDelay, initialDelay, minDelay)
			];
			return {
				remove: function () {
					handles.forEach(function (h) {
						h.remove();
					});
				}
			};
		}
	};

	return typematic;
});