event.js

Summary

DOM Event related methods.

Project Homepage: http://mozile.mozdev.org

Version: 0.8 $Id: overview-summary-event.js.html,v 1.12 2008/02/20 18:47:09 jameso Exp $

Author: James A. Overton


/* ***** BEGIN LICENSE BLOCK *****
 * Licensed under Version: MPL 1.1/GPL 2.0/LGPL 2.1
 * Full Terms at http://mozile.mozdev.org/0.8/LICENSE
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is James A. Overton's code (james@overton.ca).
 *
 * The Initial Developer of the Original Code is James A. Overton.
 * Portions created by the Initial Developer are Copyright (C) 2005-2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *	James A. Overton <james@overton.ca>
 *
 * ***** END LICENSE BLOCK ***** */

/**
 * @fileoverview DOM Event related methods.
 * <p>Project Homepage: http://mozile.mozdev.org 
 * @author James A. Overton <james@overton.ca>
 * @version 0.8
 * $Id: overview-summary-event.js.html,v 1.12 2008/02/20 18:47:09 jameso Exp $
 */


mozile.require("mozile.dom");
mozile.require("mozile.edit");
mozile.provide("mozile.event.*");

/**
 * Tools for dealing with browser events.
 * @type Object
 */
mozile.event = new Object();
// JSDoc hack
mozile.event.prototype = new mozile.Module;


/**
 * Indicates that a mouse button is being held down. 
 * @type Boolean
 */
mozile.event.mousedown = false;


/**
 * Changes some of IE's event properties to use standard names.
 * @param {Event} e The event to normalize.
 * @type Object
 */
mozile.event.normalize = function(event) {
	if(!event) return event;
	if(typeof(event.target) == "undefined") event.target = event.srcElement;
	if(typeof(event.charCode) == "undefined") event.charCode = event.keyCode;
	return event;
}

/**
 * Adds an event listener to a node.
 * @param {Element} element The element to attach the listener to.
 * @param {String} type The event type to listen for. Do NOT include the "on" prefix!
 * @param {Function} listener Optional. The function to call when the event occurs. Defailts to mozile.event.handle.
 * @param {Boolean} useCapture Optional. See the documentation for Document.addEventListener().
 * @type Void
 */
mozile.event.addListener = function(element, type, listener, useCapture) {
	if(!listener) listener = mozile.event.handle;
	if(element.addEventListener) 
		element.addEventListener(type, listener, useCapture);
	else if(element.attachEvent) 
		element.attachEvent("on" + type, listener);
	else mozile.debug.inform("mozile.event.addListener", "No known event method available");
}

/**
 * Removes an event listener from a node.
 * @param {Element} element The element to remove the listener from.
 * @param {String} type The event type. Do NOT include the "on" prefix!
 * @param {Function} listener Optional. The function to call when the event occurs. Defailts to mozile.event.handle.
 * @param {Boolean} useCapture Optional. See the documentation for Document.addEventListener().
 * @type Void
 */
mozile.event.removeListener = function(element, type, listener, useCapture) {
	if(!listener) listener = mozile.event.handle;
	if(element.removeEventListener) 
		element.removeEventListener(type, listener, useCapture);
	else if(element.detachEvent) 
		element.detachEvent("on" + type, listener);
	else mozile.debug.inform("mozile.event.addListener", "No known event method available");
}

/**
 * Set up a series of event listeners on this document.
 * @type Void
 */
mozile.event.listen = function(doc) {
	if(!doc) doc = mozile.document;
	var events = ["mousedown", "mousemove", "mouseup", "click", "dblclick", "keydown", "keyup", "keypress"];
	for(var i=0; i < events.length; i++) {
		mozile.event.addListener(doc, events[i]);
	}
}

/**
 * INCOMPLETE. Dispatch (fire) an event of a given type from the given element.
 * @param {Element} element The element fom which the event will be dispatched.
 * @param {String} type The type of event to dispatch.
 * @type Void
 */
mozile.event.dispatch = function(element, type, keyCode, charCode, ctrlKey, altKey, shiftKey, metaKey) {
	// IE event
	if(element.fireEvent) element.fireEvent("on"+type);
	// TODO: Other browsers
	else if(element.dispatchEvent) {
		var event;
		if(type.indexOf("click") > -1 || type.indexOf("mouse") > -1) {
			event = mozile.document.createEvent("MouseEvent");
			event.initMouseEvent(type, true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 1, null);
		}
		else if(type.indexOf("key") > -1) {
			if(!ctrlKey) ctrlKey = false;
			if(!altKey) altKey = false;
			if(!shiftKey) shiftKey = false;
			if(!metaKey) metaKey = false;
			event = mozile.document.createEvent("KeyEvent");
			event.initKeyEvent(type, true, true, mozile.document.defaultView, ctrlKey, altKey, shiftKey, metaKey, keyCode, charCode);
		}
		else {
			event = mozile.document.createEvent("Events");
			event.initEvent(type, true, true);
		}
		element.dispatchEvent(event);
	}
}

/**
 * Cancels an event.
 * @param {Event} event The event to cancel.
 * @type Void
 */
mozile.event.cancel = function(event) {
	if(!event) return;
	if(event.stopPropagation) event.stopPropagation();
	event.cancelBubble = true;
	if(event.preventDefault) event.preventDefault();
}

/**
 * General event handler, which calls specific event handlers for the document and for the target nodes of the event.
 * @param {Event} event The event to handle.
 * @type Boolean
 * @return False if the event was cancelled, true if it was not cancelled.
 */
mozile.event.handle = function(event) {
try {
	if(!event || !event.type) return false;
	switch(event.type) {
		case "mousemove":
			return mozile.event.handleMouseMove(event);
		case "mouseup":
		case "click":
		case "dblclick":
			mozile.event.mousedown = false;
	}

	// Get event information and make sure the target node is editable.
	event = mozile.event.normalize(event);
	mozile.event.findTarget(event);
	if(!event.node) return true;
	event.container = mozile.edit.getContainer(event.node);
	event.editable = Boolean(event.container);
	
	//if(event.type == "keyup") console.info("handle", event, event.target.ownerDocument);

	// Select non-editable objects in Mozilla.
	if(mozile.browser.isMozilla &&
		event.target.nodeType == mozile.dom.ELEMENT_NODE &&
		(event.type == "click" || event.type == "dblclick") ) {
		//console.info("Found node", event.target, event.selection.isCollapsed);
		switch(event.target.nodeName.toLowerCase()) {
			case "img":
			case "input":
			case "select":
			case "textarea":
				var IP = new mozile.edit.InsertionPoint(
					event.target.parentNode, mozile.dom.getIndex(event.target));
				if(IP) IP.select();
				if(event.type == "dblclick") {
					if(IP) IP.next();
					if(IP) IP.extend();
				}
		}
	}
	

	// Check global commands
	var state = mozile.edit.commands.trigger(event);
	if(state) return mozile.event.handled(event, state);
	
	// Set editable state
	if(mozile.gui) mozile.gui.update(event, "selection");
	if(!mozile.edit.editable) return true;
	var priorStatus = mozile.edit.status;
	mozile.edit.setStatus(event.editable);
	if(!event.editable) return true;
	
	mozile.event.storeSelection(event);
	if(!priorStatus) mozile.event.fixFocus(event);

	// Check RNG Element commands for this node and editable ancestors.
	var node = event.node;

	// This code only checks "p" commands, and only checks them once.
	// It's much faster and probably good enough for HTML editing.
	if(!this.rng) this.rng = mozile.edit.lookupRNG("p");
	if(!event.rng) event.rng = mozile.edit.lookupRNG(event.node);
	if(!event.rng) event.rng = this.rng;
	if(event.rng && event.rng.commands) {
		state = event.rng.commands.trigger(event);
		if(state) return mozile.event.handled(event, state);
	}

	// This code checks each element's RNG-defined commands, and climbs the tree.
	// This is what you want for XML editing, where the command set includes more variety.
	//while(event.node) {
	//	if(!event.rng) event.rng = mozile.edit.lookupRNG(event.node);
	//	if(event.rng && event.rng.commands) {
	//		state = event.rng.commands.trigger(event);
	//		if(state) return mozile.event.handled(event, state);
	//	}
	//	if(event.node == event.container) break;
	//	else event.node = event.node.parentNode;
	//}
	
	// Check default editing commands.
	event.node = node;
	state = mozile.edit.defaults.trigger(event);
	if(state) return mozile.event.handled(event, state);
	
	if(!mozile.event.cancelKeyEvent(event)) return false;
	if(!mozile.event.handleHyperlink(event)) return false;

} catch(e) { mozile.debug.inform("mozile.event.handle", mozile.dumpError(e)); }

	return true;
}

/**
 * Finish handling an event, eieht by cancelling it or not.
 * @param {Event} event The event to handle.
 * @type Boolean
 */
mozile.event.handled = function(event, state) {
	mozile.edit.done(state);
	if(state.changesMade) {
		//mozile.debug.debug("mozile.event.handled", state.command.name +" made change "+ state.changesMade);
		if(mozile.gui) mozile.gui.update(event, state.changesMade);
	}
	if(state.cancel) {
		mozile.event.cancel(event);
		return false;
	}
	else return true;
}

/**
 * Handles mouse move events. Part of a hack for handling selections inside links in Mozilla. See handleHyperlink() for the rest.
 * @param {Event} event The event to handle.
 * @type Boolean
 */
mozile.event.handleMouseMove = function(event) {
	if(!mozile.browser.isMozilla) return true;
	if(!mozile.event.mousedown) return true;
	var selection = mozile.dom.selection.get();
	if(!selection) return true;

	if(selection.focusNode != event.rangeParent ||
		selection.focusOffset != event.rangeOffset) {
		selection.extend(event.rangeParent, event.rangeOffset);
	}

	return true;
}

/**
 * Finds the target of an event. For a mouse event this will be the event.target. For a keyboard event we look at the selection's commonAncestorContainer.
 * @param {Event} event The event to handle.
 * @type Boolean
 */
mozile.event.findTarget = function(event) {
	if(!event || !event.type) return false;
	event.selection = mozile.dom.selection.get();
	if(event.type.indexOf("key") == 0) {
		if(!event.selection || event.selection.rangeCount < 1) return false;
		event.range = event.selection.getRangeAt(0);
		if(!event.range) return false;
		event.node = event.range.commonAncestorContainer;
		//mozile.debug.debug("mozile.event.findTarget", [event.type, event.charCode, mozile.xpath.getXPath(event.node)].join(", "));
	}
	else event.node = event.target;
	return true;
}


/**
 * Store the current selection so it can be restored. Used to maintain a selection in the text while the Mozile GUI is being used.
 * @param {Event} event The event to handle.
 * @type Void
 */
mozile.event.storeSelection = function(event) {
	//console.info("storing", event, event.node, event.target);
	mozile.dom.selection.last = {
		anchorNode: event.selection.anchorNode,
		anchorOffset: event.selection.anchorOffset, 
		focusNode: event.selection.focusNode, 
		focusOffset: event.selection.focusOffset,
		isCollapsed: event.selection.isCollapsed
	};
}

/**
 * Makes sure the editable area is focussed. This is a hack to fix a bug when activiating Mozila's designMode. The method is to duplicate the mousedown event and send it again, esentailly clicking twice.
 * @param {Event} event The event to handle.
 * @type Void
 */
mozile.event.fixFocus = function(event) {
	if(!mozile.browser.isMozilla) return;
	if(!mozile.useDesignMode) return;
	if(event.type != "mousedown") return;
	
	var newEvent = mozile.document.createEvent("MouseEvent");
	newEvent.initMouseEvent("mousedown", true, true, event.view, 1, event.screenX, event.screenY, event.clientX, event.clientY, false, false, false, false, 1, event.relatedTarget);
	event.target.dispatchEvent(newEvent);
}

/**
 * Cancels certain keyboard events before the browser handles them.
 * @param {Event} event The event to handle.
 * @type Boolean
 * @return false means the event has been cancelled.
 */
mozile.event.cancelKeyEvent = function(event) {
	// Cancel by keyCode.
	if(!event || !event.keyCode) return true;
	switch(event.keyCode) {
		case 8: // Backspace
		case 9: // Tab
		case 32: // Space
		case 46: // Delete
			mozile.event.cancel(event);
			return false;
	}

	// Cancel by charCode.
	if(!event.charCode) return true;
	switch(String.fromCharCode(event.charCode).toUpperCase()) {
		case "Z": // Undo and redo in IE
			if(mozile.browser.isIE && event.ctrlKey) {
				mozile.event.cancel(event);
				return false;
			}
			break;
	}

	return true;
}

/**
 * Handles events which would lead Mozilla to follow a hyperlink while editing.
 * @param {Event} event The event to handle.
 * @type Boolean
 * @return false means the event has been cancelled.
 */
mozile.event.handleHyperlink = function(event) {
	// Only handle certain mouse events.
	switch(event.type) {
		case "mousedown":
		case "click":
		case "dblclick":
		case "mouseup":
			break;
		default: return true;
	}

	// Allow shift-clicks.
	if(event.shiftKey) {
		var node = event.target;
		if(!node || !node.href) return true;
		if(iPub && iPub.Frame) {
			iPub.Frame.followLink(mozile.document, node.href);
			mozile.event.cancel(event);
			return false;
		}
		document.location = node.href;
		mozile.event.cancel(event);
		return false;
	}

	// Otherwise cancel the event.
	// Non-Mozilla browsers & designMode already work fine, so just return.
	if(!mozile.browser.isMozilla) return true;
	if(mozile.useDesignMode) return true; 

	var node = event.explicitOriginalTarget;
	var container = mozile.edit.getContainer(node);
	if(container) {
		while(node) {
			if(node.localName && node.localName.toLowerCase() == "a") {
				//mozile.require("mozile.util");
				//mozile.debug.debug("", mozile.util.dumpValues(event));
				if(event.selection && event.rangeParent && 
					event.rangeOffset != undefined) {
					if(event.type == "mousedown") {
						event.selection.collapse(event.rangeParent, event.rangeOffset);
						mozile.event.mousedown = true;
					}
					else mozile.event.mousedown = false;
					if(event.type == "dblclick") {
						mozile.event.selectWord(event.rangeParent, event.rangeOffset);
					}
				}
				mozile.event.cancel(event);
				return false;
			}
			if(node == container) break;
			node = node.parentNode;
		}
	}

	return true;
}

/**
 * Selects the word inside the given node which contains the given offset. Tries to immitate Mozilla's double-click behaviour.
 * @param {Node} node
 * @param {Integer} offset
 * @type Void
 */
mozile.event.selectWord = function(node, offset) {
	if(!node || offset == undefined) return;

	var selection = mozile.dom.selection.get();
	if(node.nodeType != mozile.dom.TEXT_NODE) {
		selection.collapse(node, offset);
	}

	else {
		//mozile.debug.debug("mozile.event.selectWord", mozile.xpath.getXPath(node) +" "+ offset);
		var match = /\s/;
		var data = node.data;

		// Stupid bug: the event.rangeParent is always the first text node
		// Ignore that case
		if(offset == data.length) return;
		
		var startOffset = offset - 2;
		while(startOffset >= 0) {
			if(data.charAt(startOffset).match(match)) {
				startOffset++;
				break;
			}
			else startOffset--;
		}
		if(startOffset < 0) startOffset = 0;

		var endOffset = offset + 1;
		while(endOffset <= data.length) {
			if(data.charAt(endOffset).match(match)) break;
			else endOffset++;
		}
		if(endOffset > data.length) endOffset = data.length;

		selection.collapse(node, startOffset);
		selection.extend(node, endOffset);
	}	
	
}


/**** Final Configuration ****/

// Activate the listeners.
mozile.event.listen();






Documentation generated by JSDoc on Wed Feb 20 13:25:28 2008