(function() {
// DEBUG DEFINITION
/*
* file: Debug.js
*
* @BEGINLICENSE
* Copyright 2010 Brook Novak (email : brooknovak@seaweed-editor.com)
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
* @ENDLICENSE
*/
/**
* @namespace The debug namespace will be removed in release builds.
*/
debug = {};
/**
* Asserts a condition. If a condition fails a alert box shows and an exception is thrown.
*
* @param {Boolean} cond A condition
*
* @param {String} msg An optional message
*
* @throws {Error} if a condition fails
*/
debug.assert = function(cond, msg) {
try {
if (!cond) throw new Error("Assertion failed" + (msg ? ": " + msg : ""));
} catch (e) {
var fullMsg = e.message + (e.stack ? "\nstack:\n" + e.stack : "");
alert(fullMsg);
throw e;
}
}
debug.close = function(){
debug.stop = true;
if (typeof _debugConsole != "undefined" && _debugConsole) {
_debugConsole.parentNode.removeChild(_debugConsole);
_debugConsole = null;
}
}
debug.stop = false;
/**
* Prints a message to a debug console.
*
* @param {String} msg A message to print.
*/
debug.print = function(msg){
if (debug.stop)
return;
if (window.console && console.log) {
console.log(msg);
} else if (window.opera && window.opera.postError){
window.opera.postError(msg);
} else {
// Setup debug console
if (typeof _debugConsole == "undefined" || !_debugConsole) {
_debugConsole = document.createElement("div");
_debugConsole.style.backgroundColor = "#444444";
_debugConsole.style.position = "fixed";
_debugConsole.style.border = "2px solid #000000";
_debugConsole.style.width = "320px";
_debugConsole.style.height = "174px";
_debugConsole.style.zIndex = "9999";
_debugConsole.style.top = "0";
_debugConsole.style.left = "0";
_debugConsole.innerHTML = '
close
~Seaweed~ Debug Console
';
_debugOutput = document.createElement("div");
_debugOutput.style.backgroundColor = "white";
_debugOutput.style.overflow = "scroll";
_debugOutput.style.width = _debugConsole.style.width;
_debugOutput.style.height = "140px";
_debugConsole.appendChild(_debugOutput);
}
if (document.body && _debugConsole && _debugConsole.parentNode != document.body)
document.body.appendChild(_debugConsole);
_debugOutput.innerHTML += msg.replace(/\n/g," ");
var st = _debugOutput.scrollHeight - _debugOutput.clientHeight;
if (st < 0) st = 0;
_debugOutput.scrollTop = st;
}
}
/**
* Prints a message to a debug console with a new line
*
* @param {String} msg A message to print.
*/
debug.println = function(msg){
debug.print(msg + "\n");
};
/**
* Prints out a TODO message and it's stack location
* @param {String} msg An optional todo message
*/
debug.todo = function(msg) {
try {
throw new Error();
} catch (e) {
var fullMsg = "TODO: " + (msg ? msg : "") + (e.stack ? "\nAt" + e.stack : + "");
debug.println(fullMsg);
}
};
//DEBUG DEFINITION END
// Bootstrap start
// Bootstrap end
/*
* file: Core.js
*
* @BEGINLICENSE Copyright 2010 Brook Novak (email :
* brooknovak@seaweed-editor.com) This program is free software; you can
* redistribute it and/or modify it under the terms of the GNU General
* Public License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version. This
* program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details. You should have received a copy of the GNU General Public
* License along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
* @ENDLICENSE
*/
// Short-hands which can be munged
var
// Notes:
// Not in core notation ($...) or internal notation (_...) since not ready
// for all scripts until library initialized.
/**
* The document.body reference. Available when library initialized.
*
* @type Node
*/
docBody,
/**
* @type undefined
*/
$undefined;
/**
* @namespace The main namespace for the whole system
* @author Brook Jesse Novak
*/
de = {
version : "0.0.1",
/**
* @private Module register
*/
m : [],
/**
* @namespace Contains useful collections like listed lists and
* hashsets.
* @author Brook Jesse Novak
*/
collections : {},
/**
* @namespace The DOM events subsystem.
* @author Brook Jesse Novak
*/
events : {},
/**
* Adds a callback function to be invoked once the DOM is ready. Allows
* multiple registrations of handlers. NOTE: In the debug release,
* the window "onload" events are used instead of "domready" events and
* therefore take longer to be raised than expected. This is to ensure
* that all scripts are downloaded by the bootstrapper.
*
* @type Function
* @param {Function}
* handler A call back function to be invoked when the
* document is loaded and direct edit can be initialized.
*/
onready : (function() {
var handlers = []; // zeroed when dom ready occurs
function onReadyHandler() {
if (handlers) { // First onload event?
// Fire event to handlers
for ( var i in handlers) {
handlers[i]();
}
// Mark that onload event has occured
handlers = 0;
}
}
;
// @DEBUG ON
(function() {
// @DEBUG OFF
// @DEBUG ON
// Use window onload in debug release since some broswers can
// invoke onready
// events before the bootstrapper download all scripts in the
// seaweed api.
// Register to onload event
if (window.addEventListener)
window.addEventListener("load", onReadyHandler, false);
else if (window.attachEvent)
window.attachEvent("onload", onReadyHandler);
else {
// Save exisiting handlers
if (window.onload)
handlers.push(window.onload);
window.onload = onloadFunc;
}
return;
// @DEBUG OFF
// For the release use DOMReady event - nice and quick.
// This code is based from JQuerry 1.3.2 bindReady event (MIT
// licensed code)
if (document.addEventListener) { // W3C Compliant
// Use the handy event callback
document.addEventListener("DOMContentLoaded", function() {
document.removeEventListener("DOMContentLoaded",
arguments.callee, false);
onReadyHandler();
}, false);
} else if (document.attachEvent) { // IE
// Ensure firing before onload.
// maybe late but safe also for iframes
document.attachEvent("onreadystatechange", function() {
if (document.readyState === "complete") {
document.detachEvent("onreadystatechange",
arguments.callee);
onReadyHandler();
}
});
// If IE and not an iframe continually check to see if the
// document is ready
if (document.documentElement.doScroll
&& window == window.top)
(function() {
if (!handlers)
return;
try {
// If IE is used, use the trick by Diego Perini
// http://javascript.nwbox.com/IEContentLoaded/
document.documentElement.doScroll("left");
} catch (error) {
setTimeout(arguments.callee, 0);
return;
}
// and execute any waiting functions
onReadyHandler();
})();
// Fallback: use window onload. NOTE: In release mode this
// will be present
// in this very script so it is safe to use it.
_addHandler(window, "load", function() {
onReadyHandler();
_removeHandler(window, "load", arguments.callee);
});
}
// @DEBUG ON
})();
// @DEBUG OFF
// The function
return function(handler) {
// Pending onload?
if (handlers)
handlers.push(handler);
else
handler(); // Immedite exec since onload occured
};
})(),
/**
* Prepares DEdit for usage
*/
init : function() {
// Already initialized?
if (!de.m)
return;
docBody = document.body;
var modules = de.m;
// @DEBUG ON
// @DEBUG OFF
// Initialize modules
var orderChanged;
do { // sort dependancies in a bubble sort manner
// NOTE: This does not detect cyclic dependancies... thus can
// inifitely loop
// in such cases.
// TODO: Faster/elegant way of building dependency tree
orderChanged = false;
for (var i = 0; i < modules.length; i++) {
var currentMod = modules[i];
// If this has no dependancies then leave as is
if (!currentMod.depends)
continue;
// Check that dependancies occur before this
for (var j = 0; j < currentMod.depends.length; j++) {
for (var k = i + 1; k < modules.length; k++) {
if (currentMod.depends[j] == modules[k].name) {
// Move this (modules[i]) to after the
// dependancy (modules[k])
var old = modules;
modules = old.slice(0, i).concat(
old.slice(i + 1, k + 1).concat(
[ currentMod ].concat(old
.slice(k + 1))));
orderChanged = true;
break;
}
}
if (orderChanged)
break;
}
if (orderChanged)
break;
} // Next module
} while (orderChanged); // Continue bubble sorting the dependancy
// list
// Execute initialization code
for (i in modules) {
if (modules[i].init)
modules[i].init();
}
// cleanup initialization breadcrumbs
delete de["m"];
}
};
/*
* Declare core internals inline .. this is a special setup since Core is
* the first script declared
*/
/**
*
* Enqueue's an intialization function to be invoked during the DEdit API
* initialization phase (after DOM is ready).
*
*
* If a module (script) needs to be initialized before usage and depends on
* the document DOM state to be ready - or should only be initialized when
* the API is explicitely been asked to be initialized, or initialization
* code depends on a public API interface then use this function (at most
* once per module).
*
* NOTE: All internals will be loaded upon execution of the initialization
* code, therefore there is no need to delcare dependancies (@DEPENDS or via
* this method) for usage of internals within initialization code.
*
* @param {String}
* moduleName The name of the module (script file name without
* extension).
*
* @param {Function}
* init An optional initialization method. If the initialization
* code depends on other modules' public interfaces, then specify
* the module names as additional arguments to this call.
*/
function $enqueueInit(moduleName, init) {
// @DEBUG ON
debug
.assert(de.m ? true : false,
"Attempted to enqueue initializor function after API has initialized");
for ( var i in de.m) { // Integrity check
debug.assert(de.m[i].name != moduleName,
'An initializor is already registered under the name "'
+ moduleName + '"');
}
// @DEBUG OFF
// Discover dependancies (declared as extra arguments)
var dependancies = Array.prototype.slice.call(arguments);
dependancies.splice(0, 2);
// Store the initializor for the module
de.m.push({
name : moduleName,
init : init,
depends : dependancies
});
}
;
/**
* Adds/suppliments all members to a target object from a source object. If
* the target object has a member that is also contained in source, it will
* be overridden with the source member.
*
* Leaves the source object in tact.
*
* @param {Object}
* target The destination object
*
* @param {Object}
* source The source object
*
* @param {Boolean}
* override (Optional) True to override existing members on
* conflicts, false skip conflicts. Defaults to true
*
* @return {Object} The target object
*/
function $extend(target, source, override) {
if (override !== false)
override = true;
for ( var mem in source) {
if (override || typeof target[mem] == "undefined")
target[mem] = source[mem];
}
return target;
}
/**
* Create a hash map of booleans from a comma separated set of keys.
*
* @param {String}
* str A comma separated set of keys. White spaces are not
* truncated
* @return {Object} A lookup map
*/
function $createLookupMap(str) {
var arr = str.split(",");
var map = {};
for ( var i in arr) {
map[arr[i]] = true;
}
return map;
}
/**
* Shorthand for document.createElement
*
* @param {String}
* tag The element name
* @return {Element} A new element
*/
function $createElement(tag) {
return document.createElement(tag);
}
/* Start * file: DoublyLinkedList.js */
/**
* @class
*
* A Doubly Linked List. Supports all data types. Because this collection
* creates cyclic references, you should explicitely call
* de.collections.DoublyLinkedList.clear when finished with a doubly linked
* list to avoid memory leaks in browsers which uses reference counting
* garbage collections (e.g. IE and FF).
*
* @author Brook Novak
*/
var _DoublyLinkedList = function() {
var cls = function() {
/**
* @memberOf de.collections.DoublyLinkedList Read only member that
* is the current length of the linked list.
* @type Number
*/
this.length = 0;
/**
* @memberOf de.collections.DoublyLinkedList Read only member that
* is the current head node of the linked list.
* @type de.collections.DLLNode
*/
this.head = null;
/**
* @memberOf de.collections.DoublyLinkedList Read only member that
* is the current tail node of the linked list.
* @type de.collections.DLLNode
*/
this.tail = null;
};
// Define the class body
cls.prototype = {
/**
* Adds data to the end of the linked list YYY
*
* @param {Object}
* data Data to add
*/
add : function(data) {
// create a new item object, place data in
var node = {
data : data,
next : null,
prev : null
};
// special case: no items in the list yet
if (this.length == 0) {
this.head = node;
this.tail = node;
} else {
// attach to the tail node
this.tail.next = node;
node.prev = this.tail;
this.tail = node;
}
// don't forget to update the count
this.length++;
},
/**
* Removes an item from the list.
*
* @param {Object}
* item An item to remove.
* @return {Boolean} True if item existed and was removed. False if
* the item did not exist.
*/
remove : function(item) {
var current = this.head;
while (current) {
if (current.data == item) {
removeNode(this, current);
return true;
}
current = current.next;
}
return false;
},
/**
* Removes a node at the given index.
*
* @param {Number}
* index The index to remove at.
*
* @return {Object} The removed data. Otherwise null if index out of
* bounds. Note that if data is null, null will also be
* returned.
*/
removeAtIndex : function(index) {
// check for out-of-bounds values
if (index > -1 && index < this.length) {
var current;
// Choose faster scan direction
if (index < (this.length / 2)) { // Head to tail
current = this.head;
for (var i = 0; i < index; i++) {
current = current.next;
}
} else { // Tail to head
current = this.tail;
for (var i = this.length - 1; i > index; i--) {
current = current.prev;
}
}
removeNode(this, current);
return current.data;
}
return null;
},
/**
* Removes the last node in the list and returns the item. O(1)
* operation
*
* @return {Object} The last item in the list.
*/
pop : function() {
return this.removeAtIndex(this.length - 1);
},
/**
* Removes proceding items after atNode. I.E. atNode becomes the new
* tail.
*
* @param {Object}
* atNode The which will become the new tail.
*
* @return {Boolean} True if the operation succeeded (atNode is a
* node in this linked list), False if the operation failed
* (atNode is not a node in this linked list).
*/
chop : function(atNode) {
var current = this.tail, newLength = this.length;
// Search for atNode... count how far it is from the tail
while (current && current != atNode) {
current = current.prev;
newLength--;
}
// If atNode doesn't exist, return false
if (!current)
return false;
// If atNode has a next-node (i.e. not the tail) then
// chop off the proceeding nodes
if (current.next) {
var removedNodes = current.next;
while (removedNodes) { // Kill all ref n removed nodes
removedNodes.prev.next = null;
removedNodes.prev = null;
removedNodes = removedNodes.next;
}
// Set the new tail
this.tail = current;
// Be sure to update the length
this.length = newLength;
}
return true;
},
/**
* Clears the linked list.. so list becomes empty. Call this
* whenever you are finished with a Doubly linked list to avoid
* memory leaks caused by cyclic references.
*/
clear : function() {
while (this.head) {
var node = this.head;
this.head = node.next;
node.prev = node.next = null;
}
this.tail = null;
this.length = 0;
},
/**
* Applies a function to all items in this list from the head to the
* tail.
*
* @param {Function}
* func A function to apply to each item. Takes one
* argument: the item. Return false to abort iteration.
*/
iterate : function(func) {
var current = this.head;
while (current) {
var res = func(current.data);
if (res === false)
break;
current = current.next;
}
}
}; // End clas prototype
/**
* @function
* @description An alias for de.collections.DoublyLinkedList#add
*
* @see de.collections.DoublyLinkedList#add
*/
cls.prototype.push = cls.prototype.add;
/**
* @private Removes a node from a doubly linked list
*
* @param {de.collections.DoublyLinkedList}
* dll The DLL to remove from
*
* @param {de.collections.DLLNode}
* node The node to remove - must be a node in the DLL.
*/
function removeNode(dll, node) {
if (dll.length == 1) {
dll.clear();
return;
}
if (node == dll.head) {
dll.head = node.next;
dll.head.prev = null;
node.next = null; // Avoid cyclic reference
} else if (node == dll.tail) {
dll.tail = node.prev;
dll.tail.next = null;
node.prev = null; // Avoid cyclic reference
} else {
node.prev.next = node.next;
node.next.prev = node.prev;
node.next = node.prev = null; // Avoid cyclic reference
}
dll.length--;
}
return cls;
}();
/**
* Exposure of _DoublyLinkedList internal
*
* @see _DoublyLinkedList
*/
de.collections.DoublyLinkedList = _DoublyLinkedList;
/* END OF DoublyLinkedList.js */
/*
* file: MVC.js
*
* @BEGINLICENSE Copyright 2010 Brook Novak (email :
* brooknovak@seaweed-editor.com) This program is free software; you can
* redistribute it and/or modify it under the terms of the GNU General
* Public License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version. This
* program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details. You should have received a copy of the GNU General Public
* License along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
* @ENDLICENSE
*/
/**
* Extends the given instance into a model (wrt MVC).
*
* @param {Object}
* subject The subject containing model data to be observed.
*/
function _model(subject) {
// The list of registered observers for this subject
var observers = [], observersToRemove; // Zeroed/undef when not firing
// event, array when model is
// firing event.
$extend(subject, {
/**
*
* Notifies all observers that a specific event occured.
*
* @param {String}
* event The event name to fire (excluding the "on"
* prefix). For example, "KeyDown" would invoke
* "onKeyDown" in all observers
*
* @param {Object}
* details Optional custom details data
*/
fireEvent : function(event) {
if (observers.length > 0) {
// Construct additional arguments array
var i, observer, remObservers, args = Array.prototype.slice
.call(arguments);
args.shift();
// Flag this model as firing
observersToRemove = [];
// Fire events on each observer
for (i in observers) {
observer = observers[i];
// If the observer has declared a listener function for
// this event invoke it
if (typeof observer.ref["on" + event] == "function")
observer.ref["on" + event].apply(observer.context,
args);
}
// Reset flag
remObservers = observersToRemove;
observersToRemove = 0;
// If Observers when trying to remove themself during an
// event fire
// then safely remove them now.
for (i in remObservers) {
this.removeObserver(remObservers[i]);
}
}
},
/**
* Adds an observer for receiving event notifications. If the
* observer already exists in the observer set it will not be added
* twice.
*
* @param {Object}
* observer An observer to add to the set.
*
* @param {Object}
* context (Optional) The context at which the events
* should be invoked in. Will default to the observer
* object.
*
* @param {Boolean}
* notifiedFirst (Optional) True to be the first observer
* to be notified in the current list. Otherwise it will
* be added to the end of the list
*/
addObserver : function(observer, context, notifiedFirst) {
// Ensure that observer array is a set
if (observerIndex(observer) != -1)
return;
// Create observer instance
observer = {
ref : observer,
context : context || observer
};
// Add to list depending on requested order
if (notifiedFirst)
observers.unshift(observer);
else
observers.push(observer);
},
/**
* Removes an observer from the subjects observer list.
*
* @param {Object}
* observer An observer to remove from the set
*/
removeObserver : function(observer) {
// Avoid removing observers while in a firing-event state since
// some browsers
// may miss firing an event on a observer if the observer list
// is sliced while iterating the list
if (observersToRemove)
observersToRemove.push(observer); // Will be removed after
// firing of events
// finished
else {
var index = observerIndex(observer);
if (index >= 0)
observers.splice(index, 1);
}
}
});
/**
* @param {Object}
* observerRef The observer reference to check
*
* @return {Number} The index in the observers array at which
* observerRef exists. -1 if not found.
*/
function observerIndex(observerRef) {
for (var i = 0; i < observers.length; i++) {
if (observers[i].ref == observerRef)
return i;
}
return -1;
}
}
/**
* Exposure of model internal
*
* @see _model
*/
de.model = _model;
// END MVC.js
$enqueueInit(
"Platform",
function() {
// Detect text direction locale - relies on DOM being ready for
// manipulation
var container = $createElement("p");
container.style.margin = "0 0 0 0";
container.style.padding = "0 0 0 0";
// container.style.textAlign = "start"; // If CSS 3+
container.style.textAlign = ""; // Explicitly override text
// align that might be assigned
// via style sheets
var span = $createElement("span");
span.innerHTML = "X";
container.appendChild(span);
docBody.appendChild(container);
// LTR if text position is nearer left of container, RTL if text
// position is nearer right of container
_localeDirection = span.offsetLeft < (container.offsetWidth - (span.offsetLeft + span.offsetWidth)) ? "ltr"
: "rtl";
debug.println("LOCALE-DIRECTION: " + _localeDirection + "\n");
// Tidy up
docBody.removeChild(container);
});
/**
* The clients operating system. Never null, but can be de.Platform.UNKNOWN.
*
* @type Number
*/
var _os,
/**
* The clients browser. -1 if unknown.
*
* @type Number
*/
_browser,
/**
* The browser version as a float. Can be -1 if could not determine the
* version.
*
* @type Number
*/
_browserVersion,
/**
* The clients operating system. -1 if unknown.
*
* @type Number
*/
_engine,
/**
* The layout/rendering engine. -1 if unavailable.
*
* @type Number
*/
_engineVersion,
/**
* An enumeration for browser/engine/os types. In the release version these
* are replaced with actual values.
*/
_Platform = {
/**
* Read Only
*
* @final
* @type Number
*/
UNKNOWN : -1, // @REPLACE _Platform.UNKNOWN -1
/* BROWSER CONSTANTS */
/**
* Read Only: A browser constant
*
* @final
* @type Number
*/
FIREFOX : 1, // @REPLACE _Platform.FIREFOX 1
/**
* Read Only: A browser constant
*
* @final
* @type Number
*/
OPERA : 2, // @REPLACE _Platform.OPERA 2
/**
* Read Only: A browser constant
*
* @final
* @type Number
*/
IE : 3, // @REPLACE _Platform.IE 3
/**
* Read Only: A browser constant
*
* @final
* @type Number
*/
CHROME : 4, // @REPLACE _Platform.CHROME 4
/**
* Read Only: A browser constant
*
* @final
* @type Number
*/
SAFARI : 6, // @REPLACE _Platform.SAFARI 6
// ICAB : 101, // Used as engine contant aswell
/**
* Read Only: A browser constant
*
* @final
* @type Number
*/
KONQUEROR : 8, // @REPLACE _Platform.KONQUEROR 8
/**
* Read Only: A browser constant
*
* @final
* @type Number
*/
NETSCAPE : 9, // @REPLACE _Platform.NETSCAPE 9
// OS CONSTANTS
/**
* Read Only: An OS constant
*
* @final
* @type Number
*/
WINDOWS : 1, // @REPLACE _Platform.WINDOWS 1
/**
* Read Only: An OS constant
*
* @final
* @type Number
*/
MAC : 2, // @REPLACE _Platform.MAC 2
/**
* Read Only: An OS constant
*
* @final
* @type Number
*/
LINUX : 3, // @REPLACE _Platform.LINUX 3
/* LAYOUT/RENDERING ENGINE CONSTANTS */
/**
* Read Only: A rendering engine constant
*
* @final
* @type Number
*/
GECKO : 1, // @REPLACE _Platform.GECKO 1
/**
* Read Only: A rendering engine constant
*
* @final
* @type Number
*/
TRIDENT : 2, // @REPLACE _Platform.TRIDENT 2
/**
* Read Only: A rendering engine constant
*
* @final
* @type Number
*/
WEBKIT : 3, // @REPLACE _Platform.WEBKIT 3
/**
* Read Only: A rendering engine constant
*
* @final
* @type Number
*/
KHTML : 4, // @REPLACE _Platform.KHTML 4
/**
* Read Only: A rendering engine constant
*
* @final
* @type Number
*/
PRESTO : 5
// @REPLACE _Platform.PRESTO 5
};
// Perform platform detection
(function() {
// References:
// - http://unixpapa.com/js/gecko.html
// - http://www.quirksmode.org/js/detect.html
var dataBrowser = [ {
string : navigator.userAgent,
subString : "Chrome",
id : _Platform.CHROME,
versionSearch : "Chrome"
}, {
string : navigator.vendor,
subString : "Apple",
id : _Platform.SAFARI,
versionSearch : "Version"
}, {
prop : window.opera,
id : _Platform.OPERA,
versionSearch : "Opera"
}, {
string : navigator.vendor,
subString : "KDE",
id : _Platform.KONQUEROR,
versionSearch : "Konqueror"
}, {
string : navigator.userAgent,
subString : "Firefox",
id : _Platform.FIREFOX,
versionSearch : "Firefox"
}, {
// for newer Netscapes (6+)
string : navigator.userAgent,
subString : "Netscape",
id : _Platform.NETSCAPE,
versionSearch : "Netscape"
}, {
string : navigator.userAgent,
subString : "MSIE",
id : _Platform.IE,
versionSearch : "MSIE"
}, {
// for older Netscapes (4-)
string : navigator.userAgent,
subString : "Mozilla",
id : _Platform.NETSCAPE,
versionSearch : "Mozilla"
} ];
var dataOS = [ {
string : navigator.platform,
subString : "Win",
id : _Platform.WINDOWS
}, {
string : navigator.platform,
subString : "Mac",
id : _Platform.MAC
}, {
string : navigator.platform,
subString : "Linux",
id : _Platform.LINUX
}, ];
var dataEngine = [ {
string : navigator.userAgent,
subString : "MSIE",
id : _Platform.TRIDENT,
versionSearch : "MSIE" // The trident versions are same as browser
// versions
}, {
// It is important that this is above KHTML - since webkit is forked
// from KHTML (which is still in webkits useragent/nav strings)
string : navigator.userAgent,
subString : "WebKit",
id : _Platform.WEBKIT,
versionSearch : "WebKit"
}, {
// It is important to have this above gecko data. since the user
// agent
// can contain gecko
string : navigator.userAgent,
subString : "KHTML",
id : _Platform.KHTML,
versionSearch : "KHTML"
}, {
string : navigator.userAgent,
subString : "Gecko",
id : _Platform.GECKO,
versionSearch : "rv"
}, {
prop : window.opera,
id : _Platform.PRESTO,
versionSearch : "Presto"
}, ];
function findMatchingPlatform(data) {
for ( var i in data) {
var dataString = data[i].string, dataProp = data[i].prop;
if (dataString) {
if (dataString.indexOf(data[i].subString) != -1)
return data[i];
} else if (dataProp)
return data[i];
}
return null;
}
function extractVersion(dataString, versionSearchString) {
if (!versionSearchString)
return null;
var index = dataString.indexOf(versionSearchString);
if (index == -1)
return null;
return parseFloat(dataString.substring(index
+ versionSearchString.length + 1)); // Add one for the "/"
// between the
// identifier and the
// version
}
debug.println("Inferring platform...");
// Get the OS
_os = findMatchingPlatform(dataOS);
debug.println("OS: " + (_os ? _os.subString : "UNKNOWN"));
// Set as the enum...
_os = _os ? _os.id || _Platform.UNKNOWN : _Platform.UNKNOWN;
// Get the browser
_browser = findMatchingPlatform(dataBrowser);
debug.println("BROWSER: "
+ (_browser ? _browser.versionSearch : "UNKNOWN"));
_browserVersion = _Platform.UNKNOWN;
if (_browser) {
// Extract the version
_browserVersion = extractVersion(navigator.userAgent,
_browser.versionSearch)
|| extractVersion(navigator.appVersion,
_browser.versionSearch);
// Set browser as the enum
_browser = _browser.id;
} else
_browser = _Platform.UNKNOWN;
debug.println("BROWSER-VERSION: " + _browserVersion);
// Get the layout engine
_engine = findMatchingPlatform(dataEngine);
_engineVersion = _Platform.UNKNOWN;
debug.println("ENGINE: "
+ (_engine ? (_engine.subString ? _engine.subString
: _engine.versionSearch) : "UNKNOWN"));
if (_engine) {
_engineVersion = extractVersion(navigator.userAgent,
_engine.versionSearch);
_engine = _engine.id;
} else
_engine = _Platform.UNKNOWN;
debug.println("ENGINE-VERSION: " + _engineVersion + "\n");
})();
$extend(de, {
/**
* @memberOf de Exposes internal platform enumaration
* @see _Platform
*/
Platform : _Platform,
/**
* @memberOf de Exposes internal field
* @see _os
*/
os : _os,
/**
* @memberOf de Exposes internal field
* @see _browser
*/
browser : _browser,
/**
* @memberOf de Exposes internal field
* @see _browserVersion
*/
browserVersion : _browserVersion,
/**
* @memberOf de Exposes internal field
* @see _engine
*/
engine : _engine,
/**
* @memberOf de Exposes internal field
* @see _engineVersion
*/
engineVersion : _engineVersion,
/**
* @memberOf de The Local text direction. Either "ltr" for Left to right
* or "rtl" for right to left.
* @type String
*/
localDirection : null
/* Detected once API initialized */
});
// END Platform.js
var _registerAction = function() {
alert("hit!!!");
};
(function() {
var actionRepository = {}, history = new _DoublyLinkedList(),
/*
* Always points to the next action to be undoed. Null if there is no
* undo history. Note that it can be null if there is redo history...
*/
currentActionNode = null,
/*
* Non-zero if executing an action within an action. It represents the
* action exec depth, typically it would be 0-1, but sometimes 2.
*/
execActionDepth = 0;
// Setup registor action logic
_registerAction = function(name, action) {
debug.assert(!actionRepository[name],
"Already registered action for " + name);
actionRepository[name] = action;
};
/**
* @class
*
* The undo manager singleton subject provides facilities for executing,
* undoing and redoing de.actions.UndoableAction's.
*
* Before a action is executed/undon/redone, a "onBeforeAction" event is
* fired, where the argument is the action about to be
* executed/undon/redone.
*
* After a action is executed/undon/redone, a "onAfterAction" event is
* fired, where the argument is the action that has been
* executed/undon/redone.
*
* @borrows de.mvc.AbstractSubject#addObserver as this.addObserver
*
* @borrows de.mvc.AbstractSubject#removeObserver as this.removeObserver
*/
de.UndoMan = {
/**
* Add cap to avoid consuming too much memory... < 0 = unlimited
*/
maxHistoryCount : 100,
ExecFlag : {
GROUP : 1, // @REPLACE de.UndoMan.ExecFlag.GROUP 1
UPDATE_SELECTION : 2, // @REPLACE
// de.UndoMan.ExecFlag.UPDATE_SELECTION
// 2
/**
* If provided then the undo manager will not store the undoable
* operations for undoing/redoing - It will leave the current
* operation list in tact after execution and thus the action
* will not be undone/redone directly by the undo manager.
*
* Used for executing actions within an action, or other
* internal specialized situations.
*/
DONT_STORE : 4
// @REPLACE de.UndoMan.ExecFlag.DONT_STORE 4
},
/**
* Exposure of _registerAction internal.
*
* @see _registerAction
*/
registerAction : _registerAction,
/**
*
* @param {Number}
* flags (Optional) NOTE: If currently executing in an
* action, then the DONT_STORE flag will be automatically
* set. This allows actions to be combined into one
*
* @param {String}
* actionName
*
* @return {Object} Action specific result
*/
execute : function(flags, actionName) {
// Setup arguments
var args = Array.prototype.slice.call(arguments);
// Set default flags
if (typeof flags != "number") {
flags = de.UndoMan.ExecFlag.UPDATE_SELECTION;
actionName = args[0];
args.shift();
} else
args.splice(0, 2);
// If already executing in a action
if (execActionDepth)
flags = de.UndoMan.ExecFlag.DONT_STORE;
// @DEBUG ON
// This can be helpful!!
if (execActionDepth) {
debug
.println("WARNING: executing an action within an action - Undo man setting exec flags to DONT_STORE");
}
// @DEBUG OFF
if ((flags & de.UndoMan.ExecFlag.GROUP) && !currentActionNode)
_error("Cannot group action to nothing");
// Check that the action exists
if (!actionRepository[actionName])
_error("Unknown action called \"" + actionName + "\"");
var result, action = actionRepository[actionName], actionData = new _ActionData(
actionName, flags, de.selection.getRange(false),
de.selection.getRange(true));
// Apply action filtering on selection
if (actionData.selBefore) {
var eProps = de.doc
.getEditProperties(actionData.selBefore.startNode);
if (eProps && eProps.afRE) {
var reEval = eProps.afRE.test(actionName.toLowerCase()
+ (actionName == "Format" ? args[0]
.toLowerCase() : ""));
if (reEval != eProps.afInclusive)
return;
}
}
// Notify observers
this.fireEvent("BeforeExec", actionData);
// Safety check: there shouldn't be any operations in the
// current op list if storing them here
debug.assert((flags & de.UndoMan.ExecFlag.DONT_STORE)
|| !_getOperations());
// Execute the operation
execActionDepth++;
try {
result = action.exec.apply(actionData, args);
} finally {
execActionDepth--;
}
// Add to undo history? I.E: Not returning ops
if (!(flags & de.UndoMan.ExecFlag.DONT_STORE)) {
var opList = _getOperations();
// Did anything occur?
if (!opList || opList.length == 0) {
this.fireEvent("AfterExec", actionData);
// Restore selection to state before action
if (actionData.selBefore)
de.selection.setSelection(
actionData.selBefore.startNode,
actionData.selBefore.startIndex,
actionData.selBefore.endNode,
actionData.selBefore.endIndex, true);
return result;
}
// Destroy any redo-history
// If the current undo marker is at the very beggining of
// the
// list, then reset the list.
if (!currentActionNode)
history.clear(); /*
* Note: If history is already clear
* then this is ok.
*/
else if (currentActionNode.next)
history.chop(currentActionNode);
// Store the action's operations
actionData.opList = opList;
history.push(actionData);
// Update the undo marker
currentActionNode = history.tail;
// Check for max history
if (history.length > this.maxHistoryCount
&& this.maxHistoryCount > -1
&& !(action.flags & de.UndoMan.ExecFlag.GROUP))
history.removeAtIndex(0);
}
// Should update the selection?
if (flags & de.UndoMan.ExecFlag.UPDATE_SELECTION) {
var selAfter = actionData.selAfter;
if (!selAfter)
de.selection.clear();
else
de.selection.setSelection(selAfter.startNode,
selAfter.startIndex, selAfter.endNode,
selAfter.endIndex, true);
} else {
// No need to keep the selection snapshots
delete actionData["selBefore"];
if (actionData["selAfter"])
delete actionData["selAfter"];
}
// No need to keep order selection range (only used for action
// exec benifit)
delete actionData["selBeforeOrdered"];
// Notify observers
this.fireEvent("AfterExec", actionData);
return result;
},
/**
* Undos the last action.
*/
undo : function() {
do {
// If the currentActionNode is already before the head, or
// there is
// no history, then return.
if (!currentActionNode)
return;
var actionData = currentActionNode.data;
// Notify observers
this.fireEvent("BeforeUndo", actionData);
try {
// Undo the operation
_undoOperations(actionData.opList);
// Shift the action node along
currentActionNode = currentActionNode.prev;
} catch (err) {
// If the undo failed, the all undo/redo history can
// become out of
// sync with the DOM. Therefore lose the history to
// avoid bugs from
// snowballing into something worse.
this.clear();
throw err;
}
// Restore selection
if (actionData.flags & de.UndoMan.ExecFlag.UPDATE_SELECTION) {
var selBefore = actionData.selBefore;
if (!selBefore)
de.selection.clear();
else
de.selection.setSelection(selBefore.startNode,
selBefore.startIndex, selBefore.endNode,
selBefore.endIndex, true);
}
// Notify observers
this.fireEvent("AfterUndo", actionData);
} while (currentActionNode
&& (actionData.flags & de.UndoMan.ExecFlag.GROUP));
},
/**
* Redo's the last undo.
*/
redo : function() {
var firstRedo = true;
while (1) {
if (history.length == 0
|| currentActionNode == history.tail)
return;
// Is the undo-pointer back to the very start of the
// history?
var curAction = currentActionNode ? currentActionNode.next
: history.head;
var actionData = curAction.data;
// Only continue redoing if the action if grouped
if (!firstRedo
&& !(actionData.flags & de.UndoMan.ExecFlag.GROUP))
return;
currentActionNode = curAction;
// Notify observers
this.fireEvent("BeforeRedo", actionData);
// Re-execute the operations
try {
_redoOperations(actionData.opList);
} catch (err) {
// If the undo failed, the all undo/redo history can
// become out of
// sync with the DOM. Therefore lose the history to
// avoid bugs from
// snowballing into something worse.
this.clear();
throw err;
}
// Set the new selection if it was requested to update the
// selection
// with this action
if (actionData.selAfter) {
var selAfter = actionData.selAfter;
if (!selAfter)
de.selection.clear();
else
de.selection.setSelection(selAfter.startNode,
selAfter.startIndex, selAfter.endNode,
selAfter.endIndex, true);
}
firstRedo = false;
// Notify observers
this.fireEvent("AfterRedo", actionData);
}
},
/**
* Clears all undo/redo history
*/
clear : function() {
history.clear();
currentActionNode = null;
},
/**
* @return {Boolean} True if there is any undo history.
*/
hasUndo : function() {
return currentActionNode != null;
},
/**
* @return {Boolean} True if there is any redo history.
*/
hasRedo : function() {
return history.length > 0 && currentActionNode != history.tail;
}
}; // End undo manager singleton
// Make undo manager a model
_model(de.UndoMan);
})();
var _ActionData = function() {
var cls = function(name, flags, selBefore, selBeforeOrdered) {
this.name = name;
this.flags = flags;
this.selBefore = selBefore;
this.selBeforeOrdered = selBeforeOrdered;
/* this.opList = null */
}
cls.prototype = {
/**
* @return {Node} The top-most editable section changed by this
* action. Undefined if there was none.
*/
getEditSection : function() {
if (this.opList) {
// Infer from operations list
for ( var i in this.opList) {
var op = this.opList[i];
for ( var mem in op) {
// Is this a dom node still in the document body?
if (_isDOMNode(op[mem])
&& _isAncestor(docBody, op[mem])) {
var esNode = de.doc
.getEditSectionContainer(op[mem]);
if (esNode)
return esNode;
}
}
}
}
}
};
return cls;
}();
// file: UndoMan.js END
// Start Util.js
/**
* @function Gets a text node or element in the document at a given pixel
* position.
*
* @param Number
* x The X pixel relative to the window.
* @param Number
* y The Y pixel relative to the window.
* @return {Node} An Element or Text node which i at the given coordinates.
*/
var _getRenderedNodeAtXY = document.elementFromPoint ?
/* Use native version if available */
function(x, y) {
// return document.elementFromPoint(x, y);
switch (_engine) {
case _Platform.GECKO:
case _Platform.TRIDENT:
return document.elementFromPoint(x, y);
default:
// Webkit / presto requires page coordinates instead of
// client/window coordinates
var scrollPos = _getDocumentScrollPos();
return document.elementFromPoint(x + scrollPos.left, y
+ scrollPos.top); // Opera can return text nodes
}
} : function(x, y) {
var element = null;
searchElement(docBody);
return element;
function searchElement(parent) {
// First search deeper nodes
if (parent.childNodes.length > 0) {
var child = parent.firstChild;
while (child) {
if (searchElement(child))
return true;
child = child.nextSibling;
}
}
// Test this node... if it is an element
if (parent.nodeType == Node.ELEMENT_NODE
&& (parent.offsetLeft || parent.offsetLeft == 0)) {
// Get the elements position in the window
var pos = _getPositionInWindow(parent);
// Then check to see if x/y is inside bounds
if (y >= pos.y && y <= (pos.y + parent.offsetHeight)
&& x >= pos.x && x <= (pos.x + parent.offsetWidth)) {
// Search has finished
element = parent;
return true;
}
}
return false;
}
};
/**
* Gets that position of an element in the window. Supports internal scroll
* panes - price being slower operation.
*
* @param {Object}
* ele The element to get the position for.
*
* @return {Object} The position of the given element {x,y}
*/
/*
* function _getPosInWndIntScrollSupport(ele){ var left = 0, top = 0, parent =
* ele;
*
* do { if (parent.offsetLeft || parent.offsetTop) { left +=
* parent.offsetLeft; top += parent.offsetTop; } } while (parent =
* parent.offsetParent);
*
* parent = ele; do { if (parent == docBody) break; // already handled in a
* cross-browser fashion if (parent.scrollLeft || parent.scrollTop) { left -=
* parent.scrollLeft; top -= parent.scrollTop; } } while (parent =
* parent.parentNode); // Notice here: going up parent nodes, not offsets //
* Get the document scroll var scrollPos = _getDocumentScrollPos();
* // Return coordinates relative to window (using scroll information)
* return { x: left - scrollPos.left, y: top - scrollPos.top }; }
*/
/**
* Gets the position of an element in the window. Does not garuantee to
* support internal scrolls. However does support main document scrolling.
*
* @param {Node}
* ele The element to get the position for.
*
* @return {Object} The position of the given element {x,y}
*/
function _getPosInWndFast(ele) {
var left = 0, top = 0, isFixed = 0; // True if encountered fixed element
do {
// Add pixel offset to parent
if (ele.offsetLeft || ele.offsetTop) {
if (ele == docBody) {
if (!isFixed) {
// Gecko browsers can have negitive offsets for the
// document body
// if there is a border present.
left += Math.abs(ele.offsetLeft);
top += Math.abs(ele.offsetTop);
}
} else {
left += ele.offsetLeft;
top += ele.offsetTop;
}
}
// TODO: NEED TO COMPUTE IF FIXED VIA CLASS.. expensive to do every
// element...
isFixed |= (ele.style && ele.style.position == "fixed");
} while (ele = ele.offsetParent);
if (!isFixed) {
// Subtract the document scroll for non-fixed elements
var scrollPos = _getDocumentScrollPos();
left -= scrollPos.left;
top -= scrollPos.top;
// Observations:
// IE Versions which already includes body border widths
// IE8 Standards/Quirks
// IE7 Quirks
// IE Versions which do not include body border widths
// IE7 Standards
// IE 6 Standards
if (_engine == _Platform.TRIDENT && _engineVersion < 8) {
// Some IE verions do not add the body border in any of the
// offsets (body/immediate children).
// To get the border thicknesses in IE you can query the client
// top/left which will not
// be effected by scrollbars or margins.
left += docBody.clientLeft;
top += docBody.clientTop;
}
}
// Return coordinates relative to window
return {
x : left,
y : top
};
}
/**
* @type Function
*
* Gets the position of an element in the window.
*
* @param {Node}
* ele The element to get the position for.
*
* @return {Object} The position of the given element {x,y}
*/
var _getPositionInWindow = _getPosInWndFast;
/**
* Inserts a specified DOM node after a reference element as a child of the
* reference element's parent node.
*
* @param {Node}
* newNode The dom node being inserted
*
* @param {Node}
* refNode The node after which newNode is inserted.
*
* @return {Node} newNode passed in
*/
function _insertAfter(newNode, refNode) {
var sib = refNode.nextSibling;
if (!sib)
refNode.parentNode.appendChild(newNode);
else
refNode.parentNode.insertBefore(newNode, sib);
return newNode;
}
/**
* Inserts a dom node at a given index.
*
* @param {Node}
* parentNode The parent of the newly inserted node
* @param {Node}
* newNode The dom node being inserted
* @param {Number}
* index The zero-based index of where in the parents child list
* the new node should be added.
*/
function _insertAt(parentNode, newNode, index) {
var i = -1;
var node = parentNode.firstChild;
while (++i != index && node) {
node = node.nextSibling;
}
if (i == index) {
if (i == parentNode.childNodes.length)
parentNode.appendChild(newNode);
else
parentNode.insertBefore(newNode, node);
return newNode;
}
return null;
}
/**
* Returns the combined length of all the descendant text nodes of a given
* element
*
* @param {Node}
* ele A element to get the text length for
* @return {Number} The text length for the given element
*/
function _getDeepTextLength(ele) {
var len = 0;
_visitTextNodes(ele, true, function(textNode) {
len += _nodeLength(textNode);
});
return len;
}
/**
* Clones all object members.
*
* @param {Object}
* obj An object to clone
*
* @return {Object} The cloned object.
*/
function _clone(obj) {
var clone = {};
for ( var i in obj)
clone[i] = obj[i];
return clone;
}
/**
* Traverses through DOM nodes and applies/maps a function to all nodes
*
* @param {Node}
* root The parent to search from. Inclusive in search. Null to
* find the root automatically, up to and excluding the document
* node (if any).
*
* @param {Node}
* start The node at which the traversal should start from. This
* must be a child of parent, or the same as parent.
*
* @param {Boolean}
* searchRight True to traverse tree preorder from left to right,
* e.g. current, child1, child2, ... False to traverse tree
* postorder from right to left, e.g. ... child2, child1, current
*
* @param {RegExp}
* filter A regular expression - the function is only applied to
* nodes whos names match the regular expression. If null is
* supplied then all nodes are visited.
*
* @param {Function}
* func The function to apply. One argument is given: the visting
* nodes. Returning false aborts traversal. Returning 1 in right
* searches skips traversing into the current node's children.
* Anything else returned will be ignored and the traversal will
* continue.
*/
function _visitNodes(root, start, searchRight, filter, func) {
// Ensure that root is set.
if (!root)
root = _getRoot(start, [ Node.DOCUMENT_NODE,
Node.DOCUMENT_FRAGMENT_NODE ]);
var startStack = _getAncestors(start, root, true, true);
var stackIndex = startStack.length - 1;
(function trav(parent) {
var child, res, skipChildren = false;
// Are we recursing to the starting node (building stack frame)?
if (stackIndex > 0) { // before start point
stackIndex--;
child = searchRight ? startStack[stackIndex].nextSibling
: startStack[stackIndex].previousSibling;
if (!trav(startStack[stackIndex]))
return false;
} else if (stackIndex == 0) { // start point onwards
// Map the function to the parent node if its node name isn't
// filtered out
if (searchRight && (!filter || filter.test(_nodeName(parent)))) {
res = func(parent);
if (res === false)
return res;
skipChildren = (res === 1); // Skip children?
}
// If we are traverse backwards (postorder + reverse sequence),
// then
// dont traverse deeper from the start node... begin moving
// left/upward
if (!searchRight && parent == start)
child = null;
else
child = searchRight ? parent.firstChild : parent.lastChild;
}
// Search children
if (!skipChildren) {
while (child) {
if (!trav(child))
return false;
child = (searchRight) ? child.nextSibling
: child.previousSibling;
}
}
// Map the function to the parent node if its node name isn't
// filtered out
if (!searchRight && (!filter || filter.test(_nodeName(parent))))
if (func(parent) === false)
return false;
return true;
})(root);
}
/**
* Traverses through DOM nodes and applies/maps a function to text nodes.
*
* The tree traversal is preorder.
*
* @param {Node}
* root The parent to search from. Inclusive in search. Null to
* find the root automatically.
*
* @param {Node}
* start The node at which the traversal should start from.
*
* @param {Boolean}
* searchRight True to traverse tree preorder from left to right,
* false to traverse from right to left.
*
* @param {Function}
* func The function to apply. One argument is given: the child
* text nodes. Returning false aborts traversal.
*
*
*/
function _visitTextNodes(root, start, searchRight, func) {
_visitNodes(root, start, searchRight, /^#text$/, func);
}
/**
* Traverses through DOM nodes and applies/maps a function to all nodes.
*
* The tree traversal is preorder.
*
* @param {Node}
* root The parent to search from. Inclusive in search. Null to
* find the root automatically.
*
* @param {Node}
* start The node at which the traversal should start from.
*
* @param {Boolean}
* searchRight True to traverse tree preorder from left to right,
* false to traverse from right to left.
*
* @param {Function}
* func The function to apply. One argument is given: the child
* nodes. Returning false aborts traversal.
*/
function _visitAllNodes(root, start, searchRight, func) {
_visitNodes(root, start, searchRight, null, func);
}
/**
* Determines whether a node is an ansetor of another.
*
* @param {Node}
* ancestor A dom node
*
* @param {Node}
* descendant A dom node
*
* @return {Boolean} True if ancestor is an ancestor of descendant
*/
function _isAncestor(ancestor, descendant) {
descendant = descendant.parentNode;
while (descendant) {
if (descendant == ancestor)
return true;
descendant = descendant.parentNode;
}
return false;
}
/**
* Gets ancestors of a dom node.
*
* @param {Node}
* child The node to get ancestors for.
*
* @param {Node}
* endAncestor The last ancestor of the search. This can be null
* to get all ancestors
*
* @param {Boolean}
* includeChild True to include the child in with the ancestors.
*
* @param {Boolean}
* includeEndAncestor True to include the endAncestor in with the
* ancestors.
*
* @return {Array} An array of dom Node's containinhg the ancestors. Ordered
* from child to ancestor
*/
function _getAncestors(child, endAncestor, includeChild, includeEndAncestor) {
if (child == endAncestor)
return (includeChild || includeEndAncestor) ? [ child ] : [];
var ancestors = includeChild ? [ child ] : [];
var nd = child.parentNode;
while (nd && nd != endAncestor) {
ancestors.push(nd);
nd = nd.parentNode;
}
if (includeEndAncestor && endAncestor && nd == endAncestor)
ancestors.push(endAncestor);
return ancestors;
}
/**
* Finds an ancestor for a child up to a given point with a specific
* condition.
*
* @example
*
* var firstOccuringBlock = _findAncestor(child, docBody,
* de.html.isBlockLevel, true);
*
* @param {Node}
* child The child node to begin search from (Inclusive)
*
* @param {Node}
* endAncestorEx (Optional) The ancestor of the child node to
* stop at, Null will search up to the dom tree root
*
* @param {Function}
* markFunc (Optional) A function which tests a given dom node.
* Return true to mark the node for being the node to retrieve
* (depending on stopOnFirst argument). False/null/undefined to
* continue the search.
*
* @param {Boolean}
* stopOnFirst (Optional) True to stop the search on the first
* encountered marked node, False/null/undefined to continue
* search to find last occuring marked node in ancestor path.
*
* @return {Node} The querried result - null if could not find
*/
function _findAncestor(child, endAncestorEx, markFunc, stopOnFirst) {
var lastMarkedNode = null;
while (child) {
if (markFunc && markFunc(child)) {
if (stopOnFirst)
return child;
lastMarkedNode = child;
}
if (child.parentNode == endAncestorEx)
break;
child = child.parentNode;
}
return markFunc ? lastMarkedNode : child;
}
/**
* Gets the first common ancestor between two nodes.
*
* @param {Node}
* node1 A dom node
*
* @param {Node}
* node2 A dom node
*
* @param {Boolean}
* inclusive True to count node1 and node2 as being a possible
* common ancestor.
*
* @return {Node} the first common ancestor between two nodes. Null if they
* share no ancestor.
*/
function _getCommonAncestor(node1, node2, inclusive) {
var ancestors1 = _getAncestors(node1, null, inclusive, 1), ancestors2 = _getAncestors(
node2, null, inclusive, 1), commonParent = null;
for ( var i in ancestors1) {
for ( var j in ancestors2) {
if (ancestors1[i] == ancestors2[j]) {
return ancestors1[i];
}
}
}
return null;
}
/**
* Gets the next node in the preorder/postorder traversal.
*
* @param {Node}
* node The reference point.
*
* @param {Boolean}
* searchRight True to traverse tree preorder from left to right,
* false to traverse postorder from right to left.
*
* @return {Node} The next node. Null if none exists.
*
*/
function _nextNode(node, searchRight) {
var next = null;
_visitNodes(null, node, searchRight, null, function(nd) {
if (nd == node)
return true; // Skip starting node
next = nd;
return false;
});
return next;
}
/**
* @param {Node}
* node The node to get the root for. Must not be null.
*
* @param {[Number]}
* untilNodeTypes Optional, an aray of DOM Node constants. If
* given, the search for the root node will stop just before the
* first encountered given node type. For example.
* [Node.DOCUMENT_NODE] will retreive up to the body element if
* it has a document node ancestor.
*
* @return {Node} the root of the given node
*/
function _getRoot(node, untilNodeTypes) {
while (node.parentNode) {
if (untilNodeTypes) {
for ( var i in untilNodeTypes) {
if (node.parentNode.nodeType == untilNodeTypes[i])
return node;
}
}
node = node.parentNode;
}
return node;
}
/**
* Gets a dom nodes child index in its parents childrens node list
*
* @param {Node}
* node A dom node
*
* @return {Number} The zero-based index of where the node occurs in its
* parent chilren. -1 if has no parent
*/
function _indexInParent(node) {
var index = -1;
while (node) {
index++;
node = node.previousSibling;
}
return index;
}
/**
* Returns a string so that special reserved entities and white spaces are
* escaped. Note that only whitespaces are escaped if they need to be....
*
* @param {String}
* text The text to escape.
*
* @param {Boolean}
* breakNewLines True to replace newline charactors with line
* breaks. False to treat as whitespace
*
* @return {String} The escaped version of text.
*/
function _escapeTextToHTML(text, breakNewLines) {
var escapedText = "";
var start = 0;
var c, i;
for (i = 0; i < text.length; i++) {
c = text.charAt(i);
var escapedStr = null;
switch (c) {
case "\"":
escapedStr = """;
break;
case "'":
escapedStr = "'"; // ' does not work in IE
break;
case "&":
escapedStr = "&";
break;
case "<":
escapedStr = "<";
break;
case ">":
escapedStr = ">";
break;
default:
if (breakNewLines && c == "\n") {
escapedStr = " ";
} else if (_isAllWhiteSpace(c)
&& (i == 0 || i == (text.length - 1)
|| text.charAt(i - 1) == " " || text
.charAt(i + 1) == " ")) {
escapedStr = " ";
}
}
// Does this charactor need escaping?
if (escapedStr) {
// First append charactors that are previously ok
if ((i - start) > 0) {
escapedText += (text.substring(start, i));
}
// Add the escaped version
escapedText += escapedStr;
// reset the start
start = i + 1;
}
}
// Add remaining text
if ((i - start) > 0) {
escapedText += (text.substring(start, i));
}
return escapedText;
}
/**
*
* @param {String}
* htmlText The html string to parse
*
* @return {String} The escaped version of text.
*/
function _parseHTMLString(htmlText) {
var tmp = $createElement("span");
tmp.innerHTML = htmlText;
return tmp.firstChild.nodeValue;
}
/**
* Determines whether a node is displayed or not depending on its immediate
* or inherited CSS display style. Note, that if a node's visibility is
* hidden, is does not mean it is not displayed.
*
* @param {Node}
* node A dom node to test
*
* @return {Boolean} True if the dom node is displayed, false if it is not.
*/
function _isNodeDisplayed(node) {
while (node) {
if (node.nodeType == Node.ELEMENT_NODE) {
if (node.style.display == "none")
return false;
}
node = node.parentNode;
}
return true;
}
/**
* Retreives a CSS style directly set for or inherited by a given dom node.
*
* @see www.quirksmode.org/dom/getstyles.html
*
* @param {Node}
* node A dom node.
* @param {String}
* styleProp A CSS style property, formatted in CSS notation.
* @return {String} The inherited style of the given node. Undefined if the
* node does not have the style. If the node is not an element, then
* the first ancestor element is selected.
*
*/
function _getComputedStyle(node, styleProp) {
while (node && node.nodeType != Node.ELEMENT_NODE) {
node = node.parentNode;
}
if (!node)
return;
if (window.getComputedStyle) // DOM Spec
return document.defaultView.getComputedStyle(node, "")
.getPropertyValue(styleProp)
else if (node.currentStyle) // MS HTML
return node.currentStyle[_styleCSSToJSNotation(styleProp)];
debug.println("Warning - could not get style \"" + styleProp
+ "\" for a \"" + node.nodeName + "\" element");
// Otherwise undefined...
}
/**
* Sets an element's CSS string
*
* @param {Node}
* ele An element node
* @param {String}
* css A css style string formatted in CSS notation.
*
* @example _setFullStyle(myElement, "color:red; padding:4px;
* font-size:12px");
*/
function _setFullStyle(ele, css) {
if (_engine == _Platform.TRIDENT)
ele.style.setAttribute("cssText", css);
else
ele.setAttribute("style", css);
}
/**
* Sets a CSS style value for a given element
*
* @param {Node}
* ele An element node
*
* @param {String}
* css The CSS style to set in JS Notation
*
* @param {String}
* val The value of the new style
*/
function _setStyle(ele, css, val) {
if (_engine == _Platform.TRIDENT)
ele.style.setAttribute(css, val);
else
ele.style[css] = val;
}
/**
* Retrieves the full CSS markup for an elements style. Note that this is
* not the computed markup - it is the explicitely assigned CSS for the
* particular node.
*
* @param {Node}
* ele The element to get the style from
* @return {String} The CSS for the given element. Never null, empty if no
* explicit styles set
*/
function _getFullStyle(ele) {
return (_engine == _Platform.TRIDENT ? ele.style
.getAttribute("cssText") : ele.getAttribute("style"))
|| "";
}
/**
* @param {Element}
* ele The element to check
* @return {Boolean} Evaluates to true iff the element has an element-level
* style (i.e not computed).
*/
function _doesHaveElementStyle(ele) {
var fs = _getFullStyle(ele);
// Check if the CSS text contains non-empty style-values
if (fs) {
fs = fs.split(";");
for ( var s in fs) {
var idx = fs[s].indexOf(':');
if (idx > 0 && idx < (fs[s].length - 1)
&& /\s*\S+\s*/.test(fs[s].substr(idx)))
return 1;
}
}
}
/**
* @param {String}
* styleProp A CSS style in JS notation.
* @return {String} The given CSS style in CSS notation.
*/
function _styleJSToCSSNotation(styleProp) {
do {
var match = /([A-Z])/.exec(styleProp);
if (match)
styleProp = styleProp.substr(0, match.index) + "-"
+ match[1].toLowerCase()
+ styleProp.substr(match.index + 1);
} while (match);
return styleProp;
}
;
/**
* @param {String}
* color A CSS Style color. Can be in hex, rgb, percentages or
* actual names. NOTE: Only supports converting the 16
* standardized HTML color names - unstandard color names will
* return white.
*
* @return {[Number]} An array with elements R,G and B respectively. They
* range from 0-255.
*
* DEPRECIATED
*/
var _getColorRGB = function() {
var colorRegExp = /^\s*rgb\s*\(\s*(\d+)\%?\s*\,\s*(\d+)\%?\s*\,\s*(\d+)\%?\s*\)\s*$/i,
/*
* Only going to support the 16 standardized colors. All major browsers
* support a lot more but will seriously bloat the api size.
*/
colorWMap = {
maroon : [ 128, 0, 0 ],
red : [ 255, 0, 0 ],
orange : [ 255, 165, 0 ],
yellow : [ 255, 255, 0 ],
olive : [ 128, 128, 0 ],
purple : [ 128, 0, 128 ],
fuchsia : [ 255, 0, 255 ],
white : [ 255, 255, 255 ],
lime : [ 0, 255, 0 ],
green : [ 0, 128, 0 ],
navy : [ 0, 0, 128 ],
blue : [ 0, 0, 255 ],
aqua : [ 0, 255, 255 ],
teal : [ 0, 128, 128 ],
black : [ 0, 0, 0 ],
silver : [ 12, 12, 12 ],
gray : [ 128, 128, 128 ]
};
return function(val) {
if (val.charAt(0) == "#") {
if (val.length < 7)
val += "000000";
// Convert to RGB
return [ parseInt(val.substr(1, 2), 16),
parseInt(val.substr(3, 2), 16),
parseInt(val.substr(5, 2), 16) ];
}
// Is the color in the notation "rbg(r,b,g)" ?
var match = colorRegExp.exec(val);
if (match) {
var r = parseInt(match[1]), g = parseInt(match[2]), b = parseInt(match[3]);
if (val.indexOf("%") > -1) { // convert percentages to 255
// range
if (r > 100)
r = 100; // clamp;
r = (255 * r) / 100;
if (g > 100)
g = 100; // clamp;
g = (255 * g) / 100;
if (b > 100)
b = 100; // clamp;
b = (255 * b) / 100;
} else { // Clamp 255 range
if (r > 255)
r = 255;
if (g > 255)
g = 255;
if (b > 255)
b = 255;
}
return [ r, g, b ];
}
return colorWMap[val.toLowerCase()] || [ 255, 255, 255 ];
}
}();
/**
* @param {String}
* cssStyle A CSS Style to check
* @param {String}
* val1 The value of a CSS style to compare (with val2)
* @param {String}
* val2 The value of a CSS style to compare (with val1)
* @return {Boolean} True if val1 is equivalent to val2 CSS
*
* DEPRECIATED
*
*/
var _isCSSValueSame = function() {
var fontWeightMap = {
bold : "700",
normal : "400"
};
function normalizeFontWeight(val) {
return fontWeightMap[val.toLowerCase()] || val;
}
return function(cssStyle, val1, val2) {
switch (cssStyle) {
case "backgroundColor":
case "borderColor":
case "outlineColor":
case "color":
val1 = _getColorRGB(val1);
val2 = _getColorRGB(val2);
return val1[0] == val2[0] && val1[1] == val2[1]
&& val1[2] == val2[2];
case "fontWeight":
val1 = normalizeFontWeight(val1);
val2 = normalizeFontWeight(val2);
break;
}
return val1 == val2;
};
}();
/**
* @param {String}
* styleProp A CSS style in CSS notation.
* @return {String} The given CSS style in JS notation.
*/
function _styleCSSToJSNotation(styleProp) {
do {
var index = styleProp.indexOf("-");
if (index > -1)
styleProp = (index == (styleProp.length - 1)) ? styleProp
.substr(0, index) : styleProp.substr(0, index)
+ styleProp.charAt(index + 1).toUpperCase()
+ styleProp.substr(index + 2);
} while (index > -1);
return styleProp;
}
;
/**
* Gets the outer HTML content of a given element. NOTE: Can be expensive
* for large DOM Trees in firefox/konqueror.
*
* @param {Node}
* node An Element to get it's outer HTML for.
* @return {String} The outer html of the given node.
*/
function _getOuterHTML(node) {
if (node.outerHTML)
return node.outerHTML;
else { // Firefox / konqueror
var tmp = $createElement("span");
tmp.appendChild(node.cloneNode(true));
return tmp.innerHTML;
}
}
/**
* @return {Boolean} True if this browser allows you to safely extend the
* DOM.
*/
function _isDOMExtendable() {
/* IE Versions 7 down are not core javascript. */
return !(_browser == _Platform.IE && _browserVersion < 8);
}
/**
* @param {Node}
* node The node to extract the class name from
* @param {RegExp}
* A regular expression.
* @return {String} the first occurring classname of the node which matches
* regexp. Null if did not find a match
*/
function _findClassName(node, regexp) {
if (node.nodeType == Node.ELEMENT_NODE || node == docBody) {
var clsName = _getClassName(node);
if (clsName) {
var classNames = clsName.split(' ');
for ( var i in classNames) {
if (regexp.test(classNames[i]))
return classNames[i];
}
}
}
return null;
}
/**
* @param {Node}
* element A Dom element
* @return {String} The class name for the given element
*/
function _getClassName(element) {
return element.className;
}
/**
* @param {Node}
* element A Dom element
* @param {String}
* name The class to set - overrides all classes.
*/
function _setClassName(element, name) {
return _browser == _Platform.IE ? element.setAttribute("className",
name) : element.className = name;
}
/**
* Not all browsers support Array.indexOf .. this is a manual impl.
*
* @param {Object}
* obj An object
* @param {Array}
* arr An array
* @return {Number} The index of obj in arr. -1 if obj is not in arr.
*/
function _indexOf(obj, arr) {
for ( var i in arr) {
if (arr[i] == obj)
return parseInt(i);
}
return -1;
}
/**
* @param {Node}
* node a dom node
* @return {String} The dom node's name in lower case
*/
function _nodeName(node) {
return node.nodeName.toLowerCase();
}
/**
* Determines whether a node is a text node and returns the text length if
* it is.
*
* @param {Node}
* node The dom node to test
*
* @param {Object}
* defaultValue (optional) If the node to test is not a text node
* then this value will be returned instead. Defaults to NULL.
*
* @return {Object} If the node is a text node, then the text length of the
* node will be returned. Otherwise defaultValue will be returned.
*/
function _nodeLength(node, defaultValue) {
if (typeof defaultValue == "undefined")
defaultValue = null;
return node.nodeType == Node.TEXT_NODE ? node.nodeValue.length
: defaultValue;
}
/**
* Determines if an object is a DOM Node or not.
*
* @param {Object}
* obj The object to test
*/
function _isDOMNode(obj) {
// @DEBUG ON
// In debug mode, the Node object will be created if it is not available
// - in order to provide
// node type constants. To distinuish from a real node object and the
// fabricaed one, test if the
// _DE_DEBUG_CREATED if there
if (Node._DE_DEBUG_CREATED)
return typeof obj == "object" && typeof obj.nodeType == "number"
&& typeof obj.nodeName == "string";
// @DEBUG OFF
return typeof Node == "object" ? obj instanceof Node
: (typeof obj == "object" && typeof obj.nodeType == "number" && typeof obj.nodeName == "string");
}
;
/*
* Expose internals to public
*/
$extend(de, {
/**
* Exposure of _visitAllNodes internal
*
* @see _visitAllNodes
*/
visitAllNodes : _visitAllNodes,
getCommonAncestor : _getCommonAncestor,
/**
* @param {Node}
* node a dom node
* @return {String} The inner text of the given node, never null, but
* can be empty.
*/
getInnerText : function(node) {
if (node.nodeType == Node.TEXT_NODE)
return node.nodeValue;
return node.innerText || node.textContent || "";
},
/**
* Exposure of _parseHTMLString internal
*
* @see _parseHTMLString
*/
parseHTMLString : _parseHTMLString,
/**
* Exposure of _insertAfter internal
*
* @see _insertAfter
*/
insertAfter : _insertAfter,
/**
* Exposure of _insertAt internal
*
* @see _insertAt
*/
insertAt : _insertAt,
/**
* Exposure of _findClassName internal
*
* @see _findClassName
*/
findClassName : _findClassName,
/**
* Exposure of _getPositionInWindow internal
*
* @see _getPositionInWindow
*/
getPositionInWindow : _getPositionInWindow,
getOuterHTML : _getOuterHTML,
getComputedStyle : _getComputedStyle
});
// END Util.js
// START Changes.js
(function() {
/*
* All editable sections and their initial states since startup or the
* last clear.
*/
var esStartStates = [];
$enqueueInit("Changes", function() {
de.Changes.clear();
de.doc.addObserver({
onSectionAdded : function(editSection) {
// Safety check: make sure editSection is not already
// registered
// @DEBUG ON
for ( var i in esStartStates) {
debug.assert(esStartStates[i].esNode != editSection);
}
// @DEBUG OFF
// Add the new section to the state array
esStartStates.push({
esNode : editSection,
initHTML : editSection.innerHTML
});
}
});
}, "Doc");
/**
* @class A singleton that records the editable sections that have been
* changed/added/removed over time
*/
de.Changes = {
/**
* Gets all changes since last clear
*
* @return {[Element]} A list of changed editable section nodes.
* NOTE: Does not include removed editable sections .. it
* only checks the editable section contents.
*
* @see de.Changes.clear
*/
getChangedEditableSections : function() {
var changedSections = [], stipEmptiesRE = /(<\s*\w+\s[^>]*?)(?:style|class|id|value)\s*=\s*(?:""|'')([^<]*?>)/i, stipEmptiesREPresto = /(<\s*\w+\s[^>]*?)\s*(?:style|class|id|value)\s*(>|(?:[^=][^<]*?>))/i, stripAttribWSRE = /<[^\/][^<>]*?\s[^<>]*>/, wsRE = /(?:[\t\n\r ]| )/g;
// Don't consider highlighting as part of HTML
_toggleSectionHighlight(false);
// Look for changes
for ( var i in esStartStates) {
var esSection = esStartStates[i];
if (stripIrrelevants(esSection.esNode.innerHTML) != stripIrrelevants(esSection.initHTML)
|| esSection.dirty)
changedSections.push(esSection.esNode);
}
_toggleSectionHighlight(true);
return changedSections;
/**
* Strip irrelevent html from markup when comparing differences.
* E.G. Empty attibutes or different whitespace encodings.
*
* @param {String}
* str The html to stip irrelevent data from
*/
function stripIrrelevants(str) {
// Make all whitespaces normal whitespace
str = str.replace(wsRE, " ");
var match, i, re, newStr = "";
// Strip empty attributes
// One regexp matching pass for all browsers except for
// opera...
// Opera can leave empty attrbiutes without ="".
for (i = 0; i < (_engine == _Platform.PRESTO ? 2 : 1); i++) {
// Select the regexp according to pass
re = i == 0 ? stipEmptiesRE : stipEmptiesREPresto;
while (match = re.exec(str)) {
str = str.substr(0, match.index) + match[1]
+ match[2]
+ str.substr(match.index + match[0].length);
}
}
if (match) {
// Due to attributes from being stripped must clear
// whitespaces which separate attibutes in html tags..
// since whitespaces may only be present for the empty
// tags
while (match = stripAttribWSRE.exec(str)) {
newStr += str.substr(0, match.index)
+ match[0].replace(wsRE, "");
str = str.substr(match.index + match[0].length);
}
newStr += str;
} else
newStr = str;
return de.spell.stripSpellWrapperHTML(newStr);
}
},
/**
* Wipes all recorded changes and prepares for recording new changes
* for all or a specific edit section.
*
* @param {Element}
* es (Optional) The edit section to wipe. If not
* provided then all edit sections will be wiped
*
* @see de.Changes.reset
*/
clear : function(es) {
// Exclude highlighting in HTML snapshots
_toggleSectionHighlight(false);
if (es) {
// Locate specific editable section to wipe changes
for ( var i in esStartStates) {
if (esStartStates[i].esNode == es) {
esStartStates[i].initHTML = es.innerHTML;
esStartStates[i].dirty = 0;
break;
}
}
} else {
// Wipe all previous state information
esStartStates = [];
// Build up the state information based on the current
// document state
var editableSections = de.doc.getAllEditSections();
for ( var i in editableSections) {
var domNode = editableSections[i];
esStartStates.push({
esNode : domNode,
initHTML : domNode.innerHTML
});
}
}
_toggleSectionHighlight(true);
},
/**
* Use to mark all or a specific editable section as being dirty.
*
* If marked as dirty, then the next request for changed editable
* sections will also return the editable section marked dirty even
* if they have not changed / have been cleared.
*
* The next time the edit section is cleared it will be unmarked as
* being dirty.
*
* @param {Object}
* es (Optional) The edit section to dirty. If not
* provided then all edit sections will be made dirty.
*/
dirty : function(es) {
for ( var i in esStartStates) {
if (!es || esStartStates[i].esNode == es) {
esStartStates[i].dirty = 1;
}
}
},
/**
* Clears the changes, and resets all edit sections html to their
* initial state since start up or the last clear/reset operation.
* All undo/redo history will be cleared;
*
* @see de.Changes.clear
*/
reset : function() {
// Clear undo/redo history
de.UndoMan.clear();
// Reset edit section html back to their last captured start
// states
for ( var i in esStartStates) {
esStartStates[i].esNode.innerHTML = esStartStates[i].initHTML;
}
// Setup changes again
this.clear();
}
}; // End de.Changes singleton
})();
// END CHANGES.js
// Start Clipboard.js
(function() {
$enqueueInit(
"Clipboard",
function() {
// Setup platform independant event handlers for Accel+C,
// Accel+V and Accel+X key strokes
switch (_engine) {
case _Platform.TRIDENT:
_addHandler(document, "keydown", onIEKeyDown);
break;
case _Platform.GECKO:
_addHandler(document, "keypress", onGeckoKeyPress);
break;
case _Platform.PRESTO:
_addHandler(document, "keydown", onPrestoKeyDown);
break;
case _Platform.WEBKIT:
_addHandler(document, "copy", onWKCopy);
_addHandler(document, "paste", onWKPaste);
_addHandler(document, "keydown", onWKKeyDown); // Cutting
// in
// all
// webkit,
// copy/paste
// in
// safari
// mac
break;
}
// Create the multi-lined text box for capturing clipboard
// ketstrokes
clipInputEle = $createElement("textarea");
_setFullStyle(clipInputEle,
"width:1px;height:1px;border-style:none");
clipContainer = $createElement("div");
_setClassName(clipContainer, _PROTECTED_CLASS);
_setFullStyle(clipContainer,
"position:absolute;width:1px;height:1px;display:none;z-index:-500");
clipContainer.appendChild(clipInputEle);
docBody.appendChild(clipContainer);
}, "events.Events");
/* The internal clipboard text - stored whenever a user copies. */
var intClipText,
/* The internal clipboard DOM - stored whenever a user copies. */
intClipDOM,
/*
* True if managed to copy the current internal clip text to the system
* clipboard
*/
isSysClip,
/* Used for copy/cut/paste keystroke hijacking. */
clipInputEle,
/* Used for copy/cut/paste keystroke hijacking. */
clipContainer,
/* Used for copy/cut/paste keystroke hijacking. */
clipboardTOID = null,
/*
* The cursor descriptor to restore to after a native copy. NULL if
* there is no cursor to restore.
*/
restoreCursor;
/**
* Converts a DOM tree to a textual version.
*
* @param {Node}
* node A dom tree to convert to text
* @return {String} The text equivalent of the given root node of the
* dom tree.
*/
function domToText(node) {
var text = "", child;
if (node.nodeType == Node.TEXT_NODE && _doesTextSupportNonWS(node)) {
text = node.nodeValue.replace(/[\t\n\r]/g, " "); // Make all
// HTML-whitespace
// symbols
// actual
// whitespace
} else if (node.nodeType == Node.ELEMENT_NODE) {
switch (_nodeName(node)) {
case "br":
text += "\n";
break;
case "li":
text += "\n * ";
break;
default:
if (_isBlockLevel(node))
text += "\n";
}
// Recurse
child = node.firstChild;
while (child) {
text += domToText(child);
child = child.nextSibling;
}
// Block-elements have line breaks before and after
if (_isBlockLevel(node))
text += "\n";
}
return text;
}
/**
* Copies the documents selection to the internal clipboard. Sets the
* locals intClipText, intClipDOM and isSysClip appropriatly if there is
* something to copy.
*
* @return {String} The text that is copied to the internal clipboard.
* Null if there was nothing to copy (the clipboard state will
* be unchanged in this case).
*/
function internalCopy() {
// Get the current document selections dom
var selection = de.selection.getHighlightedDOM();
if (!selection)
return null;
// Store duplicated dom
intClipDOM = selection;
// Special case: if the selection root is a list element then we
// need to
// get the list element type (ol/ul)
// TODO
// Convert DOM into text
intClipText = domToText(intClipDOM);
// Chop off leading and trailing new line if the dom tree's root is
// block level
if (_isBlockLevel(intClipDOM))
intClipText = intClipText.replace(/^\n/, "").replace(/\n$/, "");
// Reset system clip flag
isSysClip = false;
return intClipText;
}
/**
* Copies and removes the documents selection to the internal clipboard.
*
* @return {String} The text that is copied to the internal clipboard.
* Null if there was nothing to cut (the clipboard and document
* state will be unchanged in this case).
*
* @see internalCopy
*/
function internalCut() {
var res = internalCopy();
// If something was copied, remove any selection from the document
if (res)
de.selection.remove();
return res;
}
/**
* Pastes text, or DOM, into the document, if the cursor is in an
* editable section.
*
* @param {String}
* sysClipText The textual contents of the system clipboard
* if available.
*/
function internalPaste(sysClipText) {
// Don't try paste if the cursor does not exist
if (!de.cursor.exists())
return;
// Check permissions of cursor position....
// Remove the current selection if any
de.selection.remove();
var cursorDesc = de.cursor.getCurrentCursorDesc();
debug.assert(cursorDesc != null);
var textToPaste, domToPaste;
// Has there ever been anything copied internally in this session
// before?
if (intClipText) {
// If the internal clipboard content was unable to be copied to
// the system clipboard,
// then unfortunatly we will have to use this.
if (!isSysClip)
domToPaste = intClipDOM;
// If the internal clipboard text matches the system clipboard
// text, then use the
// DOM content since it is the riches content.
else if (intClipText.replace(/\s/g, "") == sysClipText.replace(
/\s/g, ""))
domToPaste = intClipDOM;
// If the system clipboard text is available, use that
else if (sysClipText)
textToPaste = sysClipText;
// If all else fails, use the internal clip text
else
domToPaste = intClipDOM;
} else
textToPaste = sysClipText; // If available, use the text in the
// system clipboard
// TMP - for debugging text pasting etc..
// if (domToPaste) {
// domToPaste = null;
// textToPaste = intClipText;
// }
// Is there anything to paste?
if (domToPaste || textToPaste) {
// Calculate the cursor index
var index = cursorDesc.relIndex;
if (cursorDesc.isRightOf)
index++;
if (_nodeName(cursorDesc.domNode) == "br")
index = 1;
var es = de.doc.getEditSectionContainer(cursorDesc.domNode);
if (es) {
var esProps = de.doc.getEditProperties(es);
if (!esProps.singleLine && domToPaste) { // Can we paste
// DOM content?
// TODO, HTML validation, DEdit filters... should this
// be in the insert HTML command?
// LOW PRIORITY
// This will take a lot of thought...
// TEMP HACK: Just past inline HTML
var inlineContentHolder = $createElement("div");
_visitAllNodes(
domToPaste,
domToPaste,
true,
function(node) {
// Add text nodes to inline content holder
if (node.nodeType == Node.TEXT_NODE)
inlineContentHolder.appendChild(node
.cloneNode(false));
else if (_isInlineLevel(node)
&& _isValidRelationship(
node,
cursorDesc.domNode.parentNode)) {
// If this node is inline and can be
// validly inserted into char position,
// see if all of its children are inline
var isAllInline = 1;
_visitAllNodes(
node,
node,
true,
function(innerNode) {
// End search once exits
// subtree
if (!_isAncestor(node,
innerNode))
return false;
// Is a node found to be
// block level?
if (_isBlockLevel(innerNode)) {
isAllInline = 0;
return false;
}
});
// If all inline/text then copy this sub
// tree
if (isAllInline
&& !(esProps.singleLine && _nodeName(node) == "br")) {
inlineContentHolder
.appendChild(node
.cloneNode(true));
return 1;
}
}
});
if (inlineContentHolder.firstChild)
de.UndoMan.execute("InsertHTML",
inlineContentHolder.innerHTML,
cursorDesc.domNode.parentNode,
cursorDesc.domNode, index);
} else if (textToPaste) { // Can we paste text content?
// Decide on insertion action and perform it
if (!esProps.singleLine && /\n/.test(textToPaste)) // If
// has
// newlines
// then
// replace
// with
// line
// breaks
de.UndoMan.execute("InsertHTML", _escapeTextToHTML(
textToPaste, true),
cursorDesc.domNode.parentNode,
cursorDesc.domNode, index);
else
de.UndoMan.execute("InsertText",
cursorDesc.domNode, textToPaste, index);
}
}
}
}
/**
* IE Only. Attempts to copy the text to the clipboard.
*
* @param {String}
* text the text to copy
* @return {Boolean} True iff the text was successfully copied to the
* system clipboard.
*/
function ieClipboardCopy(text) {
var didSucceed = window.clipboardData.setData('Text', text);
return didSucceed === $undefined || didSucceed;
}
/**
* IE Only.
*
* @return {String} The system clipboard's text. Null if unavailable
*/
function ieClipboardRetrieve() {
var clipText = window.clipboardData.getData('Text');
if (clipText === "") { // Could be empty, or failed
// Verify failure
if (!window.clipboardData.setData('Text', clipText))
clipText = null;
}
return clipText;
}
/**
* IE's On key down event
*
* @param {Event}
* e The dom event
*/
function onIEKeyDown(e) {
e = e || window.event;
if (!de.events.Keyboard.isAcceleratorDown(e))
return;
switch (e.keyCode) {
case 67: // COPY (C)
case 88: // CUT (X)
// Perform internal copy
var textToCopy = e.keyCode == 67 ? internalCopy()
: internalCut();
if (textToCopy) {
// Try to copy the text to the system clipboard the IE way
if (ieClipboardCopy(textToCopy))
isSysClip = true;
else
fallThroughCopyEvent(textToCopy);
}
break;
case 86: // PASTE (V)
var sysClipContents = ieClipboardRetrieve();
if (sysClipContents)
internalPaste(sysClipContents);
else
fallThoughPasteEvent();
break;
}
}
/**
* For mozilla platforms only.
*
* @return {Boolean} True iff this session has privileges to access
* XPConnect resources
*/
function hasXPCPriv() {
try {
if (netscape.security.PrivilegeManager.enablePrivilege)
netscape.security.PrivilegeManager
.enablePrivilege("UniversalXPConnect");
else
return false;
} catch (ex) {
return false;
}
return true;
}
/**
* Mozilla Only. Attempts to copy the text to the clipboard.
*
* @param {String}
* text the text to copy
* @return {Boolean} True iff the text was successfully copied to the
* system clipboard.
*/
function mozClipboardCopy(text) {
try {
if (!hasXPCPriv())
return false;
var str = Components.classes["@mozilla.org/supports-string;1"]
.createInstance(Components.interfaces.nsISupportsString);
str.data = text;
var trans = Components.classes["@mozilla.org/widget/transferable;1"]
.createInstance(Components.interfaces.nsITransferable);
if (!trans)
return false;
trans.addDataFlavor("text/unicode");
trans.setTransferData("text/unicode", str, copytext.length * 2);
var clipid = Components.interfaces.nsIClipboard;
var clip = Components.classes["@mozilla.org/widget/clipboard;1"]
.getService(clipid);
if (!clip)
return false;
clip.setData(trans, null, clipid.kGlobalClipboard);
} catch (e) {
// FF Sometimes throws random errors on blanks lines
return false;
}
}
/**
* Mozilla Only.
*
* @return {String} The system clipboard's text. Null if unavailable
*/
function mozClipboardRetrieve() {
try {
if (!hasXPCPriv())
return null;
var clip = Components.classes["@mozilla.org/widget/clipboard;1"]
.getService(Components.interfaces.nsIClipboard);
if (!clip)
return null;
var trans = Components.classes["@mozilla.org/widget/transferable;1"]
.createInstance(Components.interfaces.nsITransferable);
if (!trans)
return null;
trans.addDataFlavor("text/unicode");
clip.getData(trans, clip.kGlobalClipboard);
var str = {}, strLength = {}, pastetext = "";
trans.getTransferData("text/unicode", str, strLength);
if (str)
str = str.value
.QueryInterface(Components.interfaces.nsISupportsString);
if (str)
pastetext = str.data.substring(0, strLength.value / 2);
return pastetext;
} catch (e) {
// FF Sometimes throws random errors on blanks lines
return null;
}
}
/**
* Mozilla's On key press event
*
* @param {Event}
* e The dom event
*/
function onGeckoKeyPress(e) {
if (!de.events.Keyboard.isAcceleratorDown(e))
return;
switch (e.which) {
case 99: // COPY (C)
case 67:
case 120: // CUT (X)
case 88:
// Perform internal copy
var textToCopy = (e.which == 67 || e.which == 99) ? internalCopy()
: internalCut();
if (textToCopy) {
// Try to copy the text to the system clipboard the XUL way
if (mozClipboardCopy(textToCopy))
isSysClip = true;
else {
fallThroughCopyEvent(textToCopy);
}
}
break;
case 118: // PASTE (V)
case 86:
var sysClipContents = mozClipboardRetrieve();
if (sysClipContents)
internalPaste(sysClipContents);
else
fallThoughPasteEvent();
break;
}
}
/**
* Opera's On key down event
*
* @param {Event}
* e The dom event
*/
function onPrestoKeyDown(e) {
if (!de.events.Keyboard.isAcceleratorDown(e))
return;
switch (e.keyCode) {
case 67: // COPY (C)
var textToCopy = internalCopy();
if (textToCopy)
fallThroughCopyEvent(textToCopy);
break;
case 88: // CUT (X)
var textToCopy = internalCut();
if (textToCopy)
fallThroughCopyEvent(textToCopy);
break;
case 86: // PASTE (V)
fallThoughPasteEvent();
break;
}
}
/**
* Webkit's on copy event
*
* @param {Event}
* e The dom event
*/
function onWKCopy(e) {
// Webkit has a bug where the clipboard data cannot be set in the
// clipboard
// events, even though the specificatoin states that it can be set.
// Therefore
// must resort to fall-through event capturing for a workaround
if (clipboardTOID === null) { // If not currently using
// fall-through method...
var textToCopy = internalCopy();
if (textToCopy)
fallThroughCopyEvent(textToCopy);
}
}
/**
* Webkit's on paste event
*
* @param {Event}
* e The dom event
*/
function onWKPaste(e) {
// clipboardData is available for access only in this event
if (de.cursor.exists() && clipboardTOID === null) { // If not
// currently
// using
// fall-through
// method... and
// something is
// selected
internalPaste(e.clipboardData.getData("Text"));
e.preventDefault(); // NOTE: Only prevent default if pasting in
// editable section, other allow pasting in
// native controls
}
}
/**
* Webkit's on key down event (Cutting only)
*
* @param {Event}
* e The dom event
*/
function onWKKeyDown(e) {
if (de.events.Keyboard.isAcceleratorDown(e)) {
switch (e.keyCode) {
case 88: // X: Cut events in webkit dont work
var textToCopy = internalCut();
if (textToCopy)
fallThroughCopyEvent(textToCopy);
break;
case 67: // C: Copy events via keyboard in safari mac dont
// work
if (_browser == _Platform.SAFARI && _os == _Platform.MAC
&& de.cursor.exists()) {
// Observation: Safari 4 on mac does not allow copy
// events to occur if
// not coping in native text controls.
// Perform internal copy
var textToCopy = internalCopy();
if (textToCopy)
fallThroughCopyEvent(textToCopy);
}
break;
case 86: // V: Paste events via keyboard in safari mac dont
// work
if (_browser == _Platform.SAFARI && _os == _Platform.MAC
&& de.cursor.exists())
// Observation: Safari 4 on mac does not allow paste
// events to occur if
// not pasting in native text controls.
fallThoughPasteEvent();
break;
}
}
}
/**
* Invoked just before the browser is about to execute default/native
* code which copies the documents current native selection.
*
* @param {String}
* textToCopy The text to copy to the clipboard.
* Null/undefined if pasting
*/
function fallThroughCopyEvent(textToCopy) {
fallThoughClipEventBase(textToCopy);
}
function fallThoughPasteEvent() {
fallThoughClipEventBase();
}
function fallThoughClipEventBase(textToCopy) {
restoreCursor = de.cursor.getCurrentCursorDesc();
// Avoid race conditions with pending timeout
if (clipboardTOID)
clearTimeout(clipboardTOID); // Void removing text input
// event
// Set/reset the inputbox contents
clipInputEle.value = textToCopy ? textToCopy : "";
// Get the scrollbar state and set the clipboard capturer position
// in the viewport
// to avoid scrolling the document
var scrollPos = _getDocumentScrollPos();
// Position the float (container) at the top left of the viewport,
// but if the scroll bars are at zero, then place the float
// outside of the document... this will completely conceal the float
clipContainer.style.left = (scrollPos.left == 0 ? -50
: scrollPos.left + 10)
+ "px";
clipContainer.style.top = (scrollPos.top == 0 ? -50
: scrollPos.top + 10)
+ "px";
// Reveal the container
clipContainer.style.display = "";
// Select the "revealed" input box
try {
clipInputEle.focus();
clipInputEle.select();
} catch (e) {
} // Mozilla sometimes throws XPConnect security exceptions
var timeOutFunc = textToCopy ? afterNativeCopyClipInput
: afterNativePasteClipInput;
// Queue input-box removal function directly after native copy/paste
// executes
clipboardTOID = setTimeout(timeOutFunc, 0);
}
/**
* Safely hides the "temporary" clipboard input control
*/
function hideClipInput() {
clipContainer.style.display = "none";
}
/**
* Invoked after the browser natively copies the "temporary" clipboard
* input control's content
*/
function afterNativeCopyClipInput() {
clipboardTOID = null;
hideClipInput();
isSysClip = true;
window.focus();
}
/**
* Invoked after the browser natively pastes the system clipboard text
* to the "temporary" clipboard input control.
*/
function afterNativePasteClipInput() {
clipboardTOID = null;
hideClipInput();
// Ensure the cursor did not change/clear
if (restoreCursor) {
var curCursor = de.cursor.getCurrentCursorDesc();
if (!curCursor || curCursor.domNode != restoreCursor.domNode
|| curCursor.relIndex != restoreCursor.relIndex)
de.cursor.setCursor(restoreCursor);
}
internalPaste(clipInputEle.value);
window.focus();
}
})();
// End Clibpboard.js
// Start ContainerNormalization.js
/*
* TODO: MAKE HTML VALID BY PLACING INLINE ANCESTORS WITHIN THE BLOCK-LEVEL
* CONTAINERS Inlines cannot contain blocks
*
*
* ARGGG... OK JUST RUBBISH THIS.. For all invalid inlines, remove them from
* the document.. that will naturally "normalize" the range. Cannot just
* move inlines into containers because containers may contain block
* decendants.
*
* This will simplify the code a lot and keep the HTML tidy.
*
* Should design algrorithms to assume valid HTML, ad if invalid then loss
* of formatting as a result of throwing away invalid tags doesnt matter.
*/
/**
* Creates containers, adjusts structure within the given range, such that
* it garauntees that all block-level elements in the range do not share any
* ancestor inline elements that occur between the containers and the
* common-block-level-ancestor, with other block-level elements. This
* property is useful for itemizing container in a given range.
* This operation is undoable, and is intended for indentation and itemizing
* ranges.
*
* @param {Node}
* startNode The starting dom node of the range to normalize.
*
* @param {Node}
* endNode The ending dom node of the range to normalize. Can be
* the same as start node or it must occur after the start dom
* (in-order left-to-right traversal)
*
* @param {Node}
* containerTemplate (Optional) An element which supports inline
* elements to be used for inline-groups which need containers.
* Defaults to paragraph.
*
* @return {[Node]} An array containing a list of all the top-level
* containers in the given range, in order if traversing the dom
* tree in-order. Can be empty.
*
*/
function _getNormalizedContainerRange(startNode, endNode, containerTemplate) {
var template = containerTemplate || $createElement("p");
// Determine the master container for the normalization range
var masterContainer = _findAncestor(_getCommonAncestor(startNode,
endNode, true), docBody, _isBlockLevel, true)
|| docBody;
// Deepen start/end range
while (startNode.firstChild) {
startNode = startNode.firstChild;
}
while (endNode.lastChild) {
endNode = endNode.lastChild;
}
// Check if the master container allows the container template
if (masterContainer != docBody
&& !_isValidRelationship(template, masterContainer)) {
// Special case - list items
if (_nodeName(masterContainer) == "ul"
|| _nodeName(masterContainer) == "ol") {
startNode = _findAncestor(startNode, masterContainer);
endNode = _findAncestor(endNode, masterContainer);
var containers = [];
while (startNode) {
if (_nodeName(startNode) == "li")
containers.push(startNode);
startNode = startNode == endNode ? null
: startNode.nextSibling;
}
return containers;
}
// If the master container doesn't allow sub-containers, then the
// normalized range
// becomes a single container... the master container.
return [ masterContainer ]
}
// Extend range to point to begin and start at top-level sub-containers
// if they exist,
// or to the end of inline groups
var extendedRangeNode, protectedContainers = [], reinsertProcContainers = [], extendedStart, extendedEnd;
// Extend start range
_visitAllNodes(masterContainer, startNode, false, extendRange);
// Remove any protect containers
if (protectedContainers.length > 0) {
for ( var i in protectedContainers) {
if (protectedContainers[i].parentNode) {
protectedContainers[i].parentNode
.removeChild(protectedContainers[i]);
reinsertProcContainers.push(protectedContainers[i]);
}
}
}
extendedStart = extendedRangeNode;
if (!extendedStart) {
// Set start through to end of initial inline group
extendedStart = masterContainer.firstChild;
while (extendedStart.firstChild) {
extendedStart = extendedStart.firstChild;
}
}
// Extend end range
extendedRangeNode = null;
protectedContainers = [];
_visitAllNodes(masterContainer, endNode, true, extendRange);
// Remove any protect containers
if (protectedContainers.length > 0) {
for ( var i in protectedContainers) {
if (protectedContainers[i].parentNode) {
protectedContainers[i].parentNode
.removeChild(protectedContainers[i]);
reinsertProcContainers.push(protectedContainers[i]);
}
}
}
extendedEnd = extendedRangeNode;
if (!extendedEnd) {
// Set start through to end of post inline group
extendedEnd = masterContainer.lastChild;
while (extendedEnd.lastChild) {
extendedEnd = extendedEnd.lastChild;
}
}
// Move protected nodes out of the extended range if there are any
for ( var i in reinsertProcContainers) {
docBody.appendChild(reinsertProcContainers[i]);
}
// The range only containers protected nodes
if (!extendedEnd || !extendedStart)
return [];
// Perform normalization operations (two phases)
separateBlockPaths(extendedStart, extendedEnd, masterContainer);
encapsulateIGroups(extendedStart, extendedEnd, masterContainer,
template);
// Get top-level containers within the original range
var tdc = _findAncestor(startNode, masterContainer);
endNode = _findAncestor(endNode, masterContainer);
var range = [];
do {
var node = tdc;
while (node) {
if (_isBlockLevel(node)) {
range.push(node);
break;
}
node = node.firstChild;
}
tdc = tdc == endNode ? null : tdc.nextSibling;
} while (tdc);
return range;
/**
* Helper function for extending the range backward/forward. Sets the
* extendedRangeNode local iff a top-level block container is found
* within the master container
*
* @param {Node}
* domNode Provided by de.visit function
*/
function extendRange(domNode) {
if (domNode == masterContainer)
return;
// Find top-level block element from the master-container -
// inclusive of self
var node = domNode, blNode = null;
while (node != masterContainer) {
if (_isBlockLevel(node))
blNode = node;
// If this sub-tree is protected then note the protected node
// root
if (node.parentNode == masterContainer
&& de.doc.getProtectedNodeContainer(node) == node) {
protectedContainers.push(node);
blNode = null; // Ignore this since this subtree will be
// repositioned in the document
}
node = node.parentNode;
}
if (blNode) {
// If this node has a top-level block element from the
// master-container
// then the extended start has been located
extendedRangeNode = blNode;
return false;
}
}
/**
* Ensures that all first occuring block-level elements from the master
* container own their own path up to the master container.
*
* @param {Node}
* startNode The start node in the range. This must not have
* a block-level element in the path from itself up to the
* master container (exclusive).
*
* @param {Node}
* endNode The end node in the range.
*
* @param {Node}
* masterContainer The master container for all sub
* containers
*
*/
function separateBlockPaths(startNode, endNode, masterContainer) {
var scanPoint = startNode, seenEndPoint = false;
// Scan through range for all top-level block elements from the
// master container
while (scanPoint && !seenEndPoint) {
_visitAllNodes(
masterContainer,
scanPoint,
true,
function(domNode) {
scanPoint = null;
seenEndPoint |= domNode == endNode;
if (domNode == masterContainer)
return;
// If this dom node is a block element then ensure
// that it owns its own path
// up to the master container.
if (_isBlockLevel(domNode)) {
// Note: this block level element is top-level
// from the master container... i.e. there is no
// block-level elements between this node up to
// the master container
var pivotNode = domNode;
while (pivotNode.parentNode != masterContainer) {
// Separate previous siblings into duplicate
// inline node
if (pivotNode.previousSibling) {
// Clone shared inline element
var clone = pivotNode.parentNode
.cloneNode(false);
// Migrate previous siblings into duped
// inline element
while (pivotNode.previousSibling) {
var psib = pivotNode.previousSibling;
_execOp(_Operation.REMOVE_NODE,
psib);
_execOp(_Operation.INSERT_NODE,
psib, clone, 0);
}
// Insert the cloned inline element back
// into the document
_execOp(
_Operation.INSERT_NODE,
clone,
pivotNode.parentNode.parentNode,
_indexInParent(pivotNode.parentNode));
}
// Separate next siblings into duplicate
// inline node
if (pivotNode.nextSibling) {
// Clone shared inline element
var clone = pivotNode.parentNode
.cloneNode(false)
// Migrate next siblings into duped
// inline element
while (pivotNode.nextSibling) {
var nsib = pivotNode.nextSibling;
_execOp(_Operation.REMOVE_NODE,
nsib);
_execOp(_Operation.INSERT_NODE,
nsib, clone);
}
// Insert the cloned inline element back
// into the document
_execOp(
_Operation.INSERT_NODE,
clone,
pivotNode.parentNode.parentNode,
_indexInParent(pivotNode.parentNode) + 1);
}
pivotNode = pivotNode.parentNode;
} // End separating path to master container
// This dom node now owns its own path to the
// master container.
// Setup to scan down next path
scanPoint = pivotNode.nextSibling; // NB: Pivot
// is an
// immediate
// child of
// master
// container
// Check if the end node lies within this
// block-level element
_visitAllNodes(domNode, domNode, true,
function(subDomNode) {
if (subDomNode == endNode)
seenEndPoint = true;
return !seenEndPoint;
});
return false;
}
return !seenEndPoint;
});
} // End Loop: separating paths to block-level elements in given
// range
} // End function separateBlockPaths
/**
* For all sub-trees within the given range of the master container
* (i.e. Immediate children of the master continer), all sub-trees
* containing only inline elements/text nodes are migrated into a
* containerTemplate clone within the master container.
*
*
* @param {Node}
* startNode The start node to encapsulate from. This should
* not be within an inline sub-tree which has a previous
* sub-tree which contains only inline elements.
*
* @param {Node}
* endNode The end node to encapsulate to. This should not be
* within an inline sub-tree which has a following sub-tree
* which contains only inline elements.
*
* @param {Node}
* masterContainer The master container
*
* @param {Node}
* containerTemplate The container to clone for migrating
* inline groups into
*
*/
function encapsulateIGroups(startNode, endNode, masterContainer,
containerTemplate) {
// Adjust start and end nodes to their root nodes within the master
// container
startNode = _findAncestor(startNode, masterContainer);
endNode = _findAncestor(endNode, masterContainer);
var subTreeNode = startNode, inlineGroupStart = null;
// Visit all sub-trees in the master container within the range
// (i.e. immediate children in the master contianer).
while (subTreeNode) {
var domNode = subTreeNode, containsBlock = false;
while (domNode) {
if (_isBlockLevel(domNode)) {
containsBlock = true;
break;
}
// Just check down left-most-path since all block level
// elements in
// the given range have their own path to the master
// container
domNode = domNode.firstChild;
}
if (containsBlock) {
if (inlineGroupStart) {
encapsulate(inlineGroupStart, subTreeNode);
inlineGroupStart = null;
}
} else {
// Mark start of inline group - avoid including whitespace
// nodes which should not be encapsulated.
// For example, whitespace in between list items
if (!inlineGroupStart) {
if (!(subTreeNode.nodeType == Node.TEXT_NODE && !_doesTextSupportNonWS(subTreeNode)))
inlineGroupStart = subTreeNode;
}
}
subTreeNode = subTreeNode == endNode ? null
: subTreeNode.nextSibling;
}
if (inlineGroupStart)
encapsulate(inlineGroupStart, null);
/**
* Encapsulates a run of inline siblings within the master
* container.
*
* @param {Node}
* start The sibling to start encapsulating from.
*
* @param {Node}
* endEx The exclusive end sibling. Can be null for
* encapsulating all siblings from start node.
*/
function encapsulate(start, endEx) {
var igContainer = containerTemplate.cloneNode(false), igSubTree = start;
// Insert the new inline-group container into the master
// container
debug
.assert(_isValidRelationship(igContainer,
masterContainer));
_execOp(_Operation.INSERT_NODE, igContainer, masterContainer,
_indexInParent(start));
// Migrate inline run into the new container
while (igSubTree != endEx) {
var nextSib = igSubTree.nextSibling;
_execOp(_Operation.REMOVE_NODE, igSubTree);
_execOp(_Operation.INSERT_NODE, igSubTree, igContainer);
igSubTree = nextSib;
}
}
} // End function encapsulateIGroups
} // End getNormalizedContainerRange
// End ContainerNormalization.js
// StarCursor.js
(function() {
// TODO: REFACTOR de.cursor ro de.Cursor ... do when refactor de prefix
// to chosen name
/*
* A cursor descriptor that represents the current cursor that is
* showing. Null if no cursor showing
*/
var currentCursorDesc,
/* The visual representation for the cursor on the actual web page */
cursorDiv,
/* A Constant defining the time in ms between cursor blinks */
CURSOR_BLINK_MS_TIME = 460,
/* The cursor blinker timeout ID */
cursorBlinkTOId,
/*
* The x position which the cursor should realign to when moving up/down
* the content
*/
cursorXAlign,
/* Used for measurement purposes */
measureSpanEl, measureSpanTextNode, measurePreTextNode, measurePostTextNode, measureFullText,
// ------------- Define lookup maps according to the DirectEdit
// DOM-based Web Editor Specification 1.0 -------------
/*
* Refer to specification 2.2.2 Elements which the cursor can appear
* directly before
*/
beforeElements = $createLookupMap("img,table,input,select,button,textarea,object"),
/*
* Refer to specification 2.2.3 Elements which the cursor can appear
* directly after
*/
afterElements = $createLookupMap("img,table,input,select,button,textarea,object,br");
$enqueueInit("Cursor", function() {
// Create elements for measurement purposes
measureSpanEl = $createElement("span");
measureSpanTextNode = document.createTextNode("");
measureSpanEl.appendChild(measureSpanTextNode);
measurePreTextNode = document.createTextNode("");
// Create cursor div and add it to the document
cursorDiv = $createElement("div");
_setClassName(cursorDiv, _PROTECTED_CLASS + " sw-cursor"); // Avoid
// the
// cursor
// from
// being
// edited
docBody.appendChild(cursorDiv);
// Set cursor background and z-index to defaults if css sheets don't
// supply them
var cursorStyle = "", cssVal = _getComputedStyle(cursorDiv,
'z-index');
if (!cssVal || cssVal == "0" || cssVal == "auto")
;
cursorStyle += "z-index:100";
_setFullStyle(cursorDiv,
"position:absolute; width:2px;visibility:hidden;"
+ cursorStyle);
// Register to events
_addHandler(window, "resize", onWindowResized);
_addHandler(document, "keystroke", onKeyStroke);
// Make as subject
_model(de.cursor);
// Keep cursor in view after actions are executed
de.UndoMan.addObserver({
onAfterExec : scrollToCursor,
onAfterUndo : scrollToCursor,
onAfterRedo : scrollToCursor
});
}, "UndoMan");
/**
* @namespace The cursor namespace packages cursor specific operations.
*/
de.cursor = {
/**
* @class Provides flag constants for describing the cursor relation
* to the dom node.
*/
PlacementFlag : {
/**
* Read Only: Refer to specification 2.2.1. AKA Text nodes
* (containing least one renderable symbol)
*
* @type Number
*/
INSIDE : 1, // @REPLACE de.cursor.PlacementFlag.INSIDE 1
/**
* Read Only: Refer to specification 2.2.2
*
* @type Number
*/
BEFORE : 2, // @REPLACE de.cursor.PlacementFlag.BEFORE 2
/**
* Read Only: Refer to specification 2.2.3
*
* @type Number
*/
AFTER : 4
// @REPLACE de.cursor.PlacementFlag.AFTER 4
},
/**
* @param {Node}
* domNode a dom node to test
* @return {Boolean} True iff the dom node can support a cursor
* placed by it.
*/
doesNodeSupportCursor : function(domNode) {
return !de.doc.isProtectedNode(domNode)
&& de.doc.isNodeEditable(domNode);
},
/**
* Sets the new cursor. The position is updated immediatly. Set to
* null to hide/destroy the cursor.
*
* If the cursor is not in an editable area, then it will be set to
* null
*
* @param {de.cursor.CursorDescriptor}
* cursorDesc The new cursor. Null for no cursor.
*
* @return {Boolean} True if there is a cursor after the operation.
* False if the operation resulted in no cursor.
*/
setCursor : function(cursorDesc) {
// Dissallow cursor placement at protected nodes
if (cursorDesc
&& !this.doesNodeSupportCursor(cursorDesc.domNode))
cursorDesc = null;
// Set the new cursor info
currentCursorDesc = cursorDesc;
// Stop and hide the cursor blink
cursorBlink(false);
// Update cursor GUI
if (currentCursorDesc) {
// Update cursor position
cursorDiv.style.left = (currentCursorDesc.docLeft + (currentCursorDesc.isRightOf ? currentCursorDesc.width
: 0))
+ "px";
cursorDiv.style.top = currentCursorDesc.docTop + "px";
cursorDiv.style.height = currentCursorDesc.height + "px";
// Determine color for the cursor.
var color = _getComputedStyle(currentCursorDesc.domNode,
"color");
cursorDiv.style.backgroundColor = color ? color : "black";
// Begin cursor blink
cursorBlink(true);
}
// Always reset cursorXAlign
cursorXAlign = null;
// Notify observers of cursor change
this.fireEvent("CursorChanged", this.getCurrentCursorDesc());
return currentCursorDesc != null;
},
/**
* Discovers the closest matching cursor descriptor for a given
* position.
*
* @param {Number}
* targetX The X coordinate in the window from where to
* find the closest cursor position.
*
* @param {Number}
* targetY The Y coordinate in the window from where to
* find the closest cursor position.
*
* @param {Node}
* targetNode (optional) The node at the given position.
* If not provided this will be determined for you.
*
* @return {de.cursor.CursorDescriptor} A CursorInfo object
* containing the closest cursor position to the given
* target coordinates. Null if there are no valid nearby
* cursor positions.
*/
getCursorDescAtXY : getCursorDescAtXY,
/**
* @return {de.cursor.CursorDescriptor} The clone of the current
* cursor info object. Null if there is none.
*/
getCurrentCursorDesc : function() {
return currentCursorDesc ? _clone(currentCursorDesc) : null;
},
/**
* Updates the cursor GUI.
*/
refreshCursor : function() {
if (!currentCursorDesc)
return;
// IE Has this annoying event-threading model where when
// querrying some
// dom objects' properties IE instantly raises an event on the
// same trace of execution!
// This causes a nasty bug when using any of the cursor
// alrgorithms, this function
// is 'randomly' invoked during critical sections which depend
// on the measuring nodes -
// since a resize event is invoked due to querrying various
// spatial information on dom objects,
// such as getting the document.scrollLeft for rereiving a
// elements absolute position in the window.
// Work around:
if (measurePostTextNode)
return;
// Get new position
setSpatialMembers(currentCursorDesc);
// Update the gui
cursorDiv.style.left = (currentCursorDesc.docLeft + (currentCursorDesc.isRightOf ? currentCursorDesc.width
: 0))
+ "px";
cursorDiv.style.top = currentCursorDesc.docTop + "px";
cursorDiv.style.height = currentCursorDesc.height + "px";
},
/**
* Scrolls the document to view the current cursor
*/
scrollToCursor : scrollToCursor,
/**
* @return {Boolean} True if the cursor is shown. False if no cursor
* exists.
*/
exists : function() {
return currentCursorDesc != null;
},
/**
* @param {Node}
* node A DOM node to test
* @return {Boolean} True if node is the cursor blinker element.
*/
isCursorEle : function(node) {
return node == cursorDiv;
},
/**
* A wrapper function for _getRenderedNodeAtXY - excludes
*
* @param {Number}
* x The x pixel coordinate relative to the window.
* @param {Number}
* y The x pixel coordinate relative to the window.
* @return {Node} The node at the given window position - gauranteed
* not to be the cursor blinker.
*/
getNonCursorNodeAtXY : function(x, y) {
var dval = cursorDiv.style.display;
cursorDiv.style.display = "none"; // Ensure not visible
var targetNode = _getRenderedNodeAtXY(x, y);
cursorDiv.style.display = dval; // Restore original value
return targetNode;
},
/**
* Gets the next cursor descriptor before or after a given cursor
* descriptor. I.E. The next physical move of the cursor.
*
* @param {de.cursor.CursorDescriptor}
* cursorDesc The cursor descriptor to reference from. it
* does not have to be a valid cursor descriptor, that
* is, a node which the cursor cannot be place in/next
* to.
*
* @param {Boolean}
* left True to get cursor desc left of reference point,
* false for right.
*
* @return {de.cursor.CursorDescriptor} The next cursor move from
* the reference point. Null if there is none. Note that it
* might be outside of an editable section.
*
*/
getNextCursorMovement : getNextCursorMovement,
/**
* Note: only uses y and height members
*
* @param {de.cursor.CursorDescriptor}
* desc1 A cursor to compare
*
* @param {de.cursor.CursorDescriptor}
* desc2 A cursor to compare
*
* @return {Boolean} True iff desc1 and desc2 are on the same line.
*/
isOnSameLine : isOnSameLine,
/**
* Builds a cursor descriptor (provides the spatial information).
*
* @param {Node}
* domNode The dom node
*
* @param {Number}
* relIndex The index within the dom node (only applies
* for text nodes)
*
* @param {Boolean}
* isRightOf True if the cursor is to the right of index
* position.
*
* @return {de.cursor.CursorDescriptor} The cursor descriptor. Null
* If a cursor cannot be placed at the given position. Note:
* if the dom not is a text node but the index is not
* rendered then null will be returned.
*
*/
createCursorDesc : function(domNode, relIndex, isRightOf) {
var placement = getPlacementFlags(domNode);
if (placement == 0
|| (placement != de.cursor.PlacementFlag.INSIDE && ((isRightOf && !(placement & de.cursor.PlacementFlag.AFTER)) || (!isRightOf && !(placement & de.cursor.PlacementFlag.BEFORE)))))
return null;
var desc = {
domNode : domNode,
relIndex : relIndex,
isRightOf : isRightOf,
placement : placement
};
setSpatialMembers(desc);
// If the text node / index is not rendered then return null
if (placement == de.cursor.PlacementFlag.INSIDE
&& (desc.width == 0 || desc.height == 0))
return null;
return desc;
},
/**
* Note: even if de.cursor.PlacementFlag.INSIDE is returned, the
* text node may not be able to support a cursor being placed inside
* if it contains nothing but non-renderable symbols.
*
* @param {Node}
* node A dom node to get the placement flags for
*
* @return {Number} A bitwise or combination of
* de.cursor.PlacementFlag's. Zero if it is not a candidate
* for supporting a cursor placement.
*/
getPlacementFlags : getPlacementFlags,
/**
* Gets the nearest cursor descriptor to the given position
* information.
*
* @param {Node}
* domNode A dom node in the document
*
* @param {Number}
* relIndex A relative index in the dom node
*
* @param {Boolean}
* isRightOf True if the cursor is placed to the right of
* the node/index. False for left placement.
*
* @param {Boolean}
* searchLeft True to search for nearest cursor to the
* left of the given position. False to search right.
*
* @return {de.cursor.CursorDescriptor} A cursor descriptor nearest
* to the given position. Null if there is none.
*/
getNearestCursorDesc : function(domNode, relIndex, isRightOf,
searchLeft) {
if (_nodeName(domNode) == "br")
isRightOf = true;
var cDesc = de.cursor.createCursorDesc(domNode, relIndex,
isRightOf); // Gives null if invalid request
// Special case: line breaks
if (cDesc && _nodeName(domNode) == "br") {
// Get the cursor pos to the left and right of this line
// break
var leftCDesc = getNextCursorMovement(cDesc, true), rightCDesc = getNextCursorMovement(
cDesc, false);
// If this line break is on its own line then return it
if ((!leftCDesc || !isOnSameLine(leftCDesc, cDesc))
&& (!rightCDesc || !isOnSameLine(rightCDesc, cDesc)))
return cDesc;
// Otherwise return the cursor pos to the left/right of the
// break - if it exists
return (searchLeft ? leftCDesc : rightCDesc) || cDesc;
}
return cDesc || getNextCursorMovement({
domNode : domNode,
isRightOf : isRightOf,
relIndex : relIndex,
y : _getPositionInWindow(domNode).y
}, searchLeft);
} // End getNearestCursorDesc function
}; // End Cursor Namespace
/* -------------------------------------------------------------------------------------- */
// Events
/* -------------------------------------------------------------------------------------- */
function onKeyStroke(e, normalizedKey) {
if (!currentCursorDesc || e.ctrlKey || e.metaKey || e.altKey)
return;
var moveAction = 0;
switch (normalizedKey) {
case "Left": // Arrow left
case "Right": // Arrow right
moveAction = 1;
// Attempt to move the cursor left/right
var neighbour = getNextCursorMovement(currentCursorDesc,
normalizedKey == "Left");
// If the neighbour is null, then reached end point
if (!neighbour)
neighbour = currentCursorDesc;
var oldCDesc = currentCursorDesc;
if (!de.cursor.setCursor(neighbour) && oldCDesc)
// If failed to set the cursor (probably due to it being
// outside
// of editable section) then set back to the old cursor
// position
de.cursor.setCursor(oldCDesc)
break;
case "Up": // Arrow up
case "Down": // Arrow down
moveAction = 1;
var isUpward = normalizedKey == "Up";
var docScrollPos = _getDocumentScrollPos();
var xAlign = cursorXAlign ? cursorXAlign
: (currentCursorDesc.docLeft - docScrollPos.left)
+ (currentCursorDesc.isRightOf ? currentCursorDesc.width
: 0);
var searchSpace = getUpDownSearchSpace();
var cDesc;
if (searchSpace) {
// Perform specialized/narrowed dual binary search to
// discover cursor position directly above/below
cDesc = searchBestCursorPos(xAlign,
currentCursorDesc.docTop - docScrollPos.top,
searchSpace, currentCursorDesc, isUpward);
restoreMeasuringNodes();
}
if (!cDesc)
cDesc = currentCursorDesc;
var oldCDesc = currentCursorDesc;
if (!de.cursor.setCursor(cDesc) && oldCDesc)
// If failed to set the cursor (probably due to it being
// outside
// of editable section) then set back to the old cursor
// position
de.cursor.setCursor(oldCDesc)
// Remember for next up/down movement
cursorXAlign = xAlign;
break;
} // End case
if (moveAction) {
scrollToCursor(); // Auto scroll
return false; // Consume key event
}
return true;
// Inner support functions to follow
/**
* An inner supporting function.
*
* @return {Object} The search space for a dual binary search
*/
function getUpDownSearchSpace() {
// Build array of all nodes to search inside of targetEl
var nodesToSearch = [];
var pendingBANodes = [];
var nextLine;
_visitAllNodes(
docBody,
currentCursorDesc.domNode,
!isUpward,
function(domNode) {
// Add in any pending before/after nodes
appendPendingBAnodes(domNode);
var placementFlags = getPlacementFlags(domNode);
if (placementFlags == 0)
return true; // Cursor cannot be placed in
// this node
// Check to see if found a new line
var pos, height;
if (domNode.nodeType == Node.TEXT_NODE) {
pos = _getPositionInWindow(domNode.parentNode);
height = domNode.parentNode.offsetHeight;
} else if (_nodeName(domNode) == "br") {
pos = measureLineBreak(domNode)
height = pos.height;
} else {
pos = _getPositionInWindow(domNode);
height = domNode.offsetHeight;
}
if (!isOnSameLine(nextLine ? nextLine
: currentCursorDesc, {
y : pos.y,
height : height
})) {
// If already found the next line then the
// searchspace can end here
if (nextLine)
return false;
// If this is the first node that is in the next
// line, then remeber line information
nextLine = {
y : pos.y,
height : height
};
}
if (placementFlags == de.cursor.PlacementFlag.INSIDE) { // AKA
// a
// text
// node
nodesToSearch.push({
domNode : domNode,
placement : de.cursor.PlacementFlag.INSIDE
});
} else { // Before and/or After
if (placementFlags
& de.cursor.PlacementFlag.BEFORE) {
if (isUpward) {
pendingBANodes.push(domNode);
} else {
nodesToSearch
.push({
domNode : domNode,
placement : de.cursor.PlacementFlag.BEFORE
// Only store before flag
});
}
}
if (placementFlags
& de.cursor.PlacementFlag.AFTER) {
if (isUpward) {
nodesToSearch
.push({
domNode : domNode,
placement : de.cursor.PlacementFlag.AFTER
// Only store before flag
});
} else {
pendingBANodes.push(domNode);
}
}
}
});
// Was the next line even found?
if (!nextLine)
return null;
// Append any pending before/after nodes
appendPendingBAnodes(null);
// Make sure search space is top-down
if (isUpward)
nodesToSearch.reverse();
// Suppliment search space with index information
var totalPlacementLength = 0;
for ( var i in nodesToSearch) {
var node = nodesToSearch[i];
var len = _nodeLength(node.domNode, 1);
node.startIndex = totalPlacementLength;
node.endIndex = node.startIndex + (len - 1);
node.length = len;
totalPlacementLength += len;
}
return {
nodes : nodesToSearch,
totalLength : totalPlacementLength
};
/**
* Adds pending before/after nodes to the nodesToSearch
*
* @param {Node}
* domNode the current dom node or null
*/
function appendPendingBAnodes(domNode) {
while (pendingBANodes.length > 0) {
if (!domNode
|| !_isAncestor(
pendingBANodes[pendingBANodes.length - 1],
domNode)) {
nodesToSearch
.push({
domNode : pendingBANodes.pop(),
placement : isUpward ? de.cursor.PlacementFlag.BEFORE
: de.cursor.PlacementFlag.AFTER
});
} else
break;
}
} // End inner appendPendingBAnodes
} // End inner getUpDownSearchSpace
} // End onKeyStroke
/**
* If the window resized, the cursor must be re-positioned.
*
* @param {Event}
* e
*/
function onWindowResized(e) {
de.cursor.refreshCursor();
}
/* -------------------------------------------------------------------------------------- */
// Support functions for cursor GUI
/* -------------------------------------------------------------------------------------- */
/**
* @param {Boolean}
* visible True to toggle cursor div to visible, false to
* hidden.
*/
function setCursorVisible(visible) {
cursorDiv.style.visibility = visible ? "visible" : "hidden";
}
/**
* @return {Boolean} True if the cursor div is visisble.
*/
function isCursorVisible() {
return cursorDiv.style.visibility == "visible";
}
/**
* Continuously toggles the cursor div's visibility over time
*
* @param {Boolean}
* on True to turn it on, false to turn it off and hide it.
*/
function cursorBlink(on) {
if (typeof on != "undefined")
cursorBlink.on = on;
if (cursorBlink.on) {
setCursorVisible(!isCursorVisible());
cursorBlinkTOId = setTimeout(cursorBlink, CURSOR_BLINK_MS_TIME,
1); // NB: IE does not pass arguments
} else if (cursorBlinkTOId) {
clearTimeout(cursorBlinkTOId);
cursorBlinkTOId = null;
setCursorVisible(false);
}
}
/**
* Scrolls the document to the current cursor's position
*/
function scrollToCursor() {
if (!currentCursorDesc)
return;
var viewPortSize = _getViewPortSize(), dx = 0, dy = 0;
// Get dy
if ((currentCursorDesc.y + currentCursorDesc.height) >= viewPortSize.height) {
dy = (currentCursorDesc.y + currentCursorDesc.height)
- viewPortSize.height;
} else if (currentCursorDesc.y < 0) {
dy = currentCursorDesc.y;
}
// get dx
var xpos = currentCursorDesc.x
+ (currentCursorDesc.isRightOf ? currentCursorDesc.width
: 0) + parseInt(cursorDiv.style.width);
if (xpos >= viewPortSize.width) {
dx = xpos - viewPortSize.width;
} else if (currentCursorDesc.x < 0) {
dx = currentCursorDesc.x;
}
if (dx || dy)
window.scrollBy(dx, dy);
}
/* -------------------------------------------------------------------------------------- */
// Support functions for locating cursor information
/* -------------------------------------------------------------------------------------- */
// See de.cursor.getPlacementFlags doc
function getPlacementFlags(node) {
// Always place cursor after the packages.
// NOTE: Testing this first to avoid placing cursor in placeholders
// within the packaged nodes.
var pcon = de.doc.getPackageContainer(node);
if (pcon) {
var pflags = 0;
if (pcon == node)
pflags = de.cursor.PlacementFlag.BEFORE
| de.cursor.PlacementFlag.AFTER;
return pflags;
}
// Cursors can be placed before placeholders, but not inside of them
if (de.doc.isESPlaceHolder(node, false))
return (de.doc.isESPlaceHolder(node, true)) ? de.cursor.PlacementFlag.BEFORE
: 0;
if (de.doc.isMNPlaceHolder(node, false))
return (de.doc.isMNPlaceHolder(node, true)) ? de.cursor.PlacementFlag.BEFORE
: 0;
if (node.nodeType == Node.TEXT_NODE) {
if (_doesTextSupportNonWS(node))
return de.cursor.PlacementFlag.INSIDE;
return 0;
}
var flags = 0;
// TODO: REFACTOR/DOCUMENT
// Apply user flag funciton if specified
if (de.cursor.usrGetPlacementFlags) {
flags = de.cursor.usrGetPlacementFlags(node);
if (flags === $undefined)
flags = 0;
else
return flags;
}
if (beforeElements[_nodeName(node)])
flags = de.cursor.PlacementFlag.BEFORE;
if (afterElements[_nodeName(node)])
flags |= de.cursor.PlacementFlag.AFTER;
return flags;
}
/**
* Determines whether cinf1 is closer to target than cinf2. Uses adx/ady
* information
*
* @param {de.cursor.CursorDescriptor}
* desc1 A cursor to compare
* @param {de.cursor.CursorDescriptor}
* desc2 A cursor to compare
* @param {Number}
* targetY The target Y (used for boundry cases)
*
* @return True if desc1 is closer than desc2.
*/
function isCloserToTarget(desc1, desc2, targetY) {
// If they are on the same line...
if (isOnSameLine(desc1, desc2)) {
// Then compare there absolute delta x's
return desc1.adx < desc2.adx;
} else if (desc1.ady == desc2.ady) {
// Else if not on the same line AND they have the same absolute
// delta y's
// then choose the closest to target Y from in the middle of
// their height
return Math.abs((desc1.y + (desc1.height / 2)) - targetY) < Math
.abs((desc2.y + (desc2.height / 2)) - targetY);
}
// They do not accur on the same line, and have diffent ady's... so
// pick closest to target Y
return desc1.ady < desc2.ady;
}
/**
* Determines whether desc1 is closer to (x, y) than desc2.
*
* @param {de.cursor.CursorDescriptor}
* desc1 A cursor to compare
* @param {de.cursor.CursorDescriptor}
* desc2 A cursor to compare
* @param {Number}
* tx The target x-coord
* @param {Number}
* ty The target y-coor
*
* @return True if desc1 is closer than desc2.
*/
function isCloserToXY(desc1, desc2, x, y) {
// If they are on the same line then compare there absolute delta
// x's
if (isOnSameLine(desc1, desc2))
return Math.abs(desc1.x - x) < Math.abs(desc2.x - x);
var ady1 = Math.min(Math.abs(desc1.y - y), Math
.abs((desc1.y + desc1.height) - y));
var ady2 = Math.min(Math.abs(desc2.y - y), Math
.abs((desc2.y + desc2.height) - y));
// Else if not on the same line AND they have the same absolute
// delta y's
// then choose the closest to target Y from in the middle of their
// height
if (ady1 == ady2)
return Math.abs((desc1.y + (desc1.height / 2)) - y) < Math
.abs((desc2.y + (desc2.height / 2)) - y);
// They do not accur on the same line, and have diffent ady's... so
// pick closest to target Y
return ady1 < ady2;
}
// See de.cursor.isOnSameLine jsdoc
function isOnSameLine(desc1, desc2) {
return (desc1.y >= desc2.y && desc1.y < (desc2.y + desc2.height))
|| (desc2.y >= desc1.y && desc2.y < (desc1.y + desc1.height));
}
/**
* If there are any elements/nodes in the document that were used for
* measurement purposes (via setupMeasuringNodes).
*/
function restoreMeasuringNodes() {
if (measurePostTextNode) {
measurePostTextNode.parentNode.removeChild(measureSpanEl);
measurePostTextNode.parentNode.removeChild(measurePreTextNode);
measurePostTextNode.nodeValue = measureFullText;
measurePostTextNode = null;
}
}
/**
* Lazily sets up measuring elements/nodes in document. Be sure to call
* restoreMeasuringNodes if you want the nodes to be removed (i.e. The
* DOM document to return to it's original state)
*
* @param {Node}
* textNode The text-node for which measurements are to take
* place.
*/
function setupMeasuringNodes(textNode) {
if (measurePostTextNode == textNode)
return;
if (measurePostTextNode)
restoreMeasuringNodes();
measurePostTextNode = textNode;
measureFullText = textNode.nodeValue;
// Split the text node into 3 nodes (including self)
textNode.parentNode.insertBefore(measureSpanEl, textNode);
textNode.parentNode.insertBefore(measurePreTextNode, measureSpanEl);
}
/**
* Requires that setupMeasuringNodes has been invoked. Isolates measure
* nodes so that the measureSpanEl encapsulates a single charactor at a
* given index.
*
* @param {Number}
* index The index of the charactor to isolate.
* @return True if the isolated charactor is a renderable symbol.
*/
function measureCharactor(index) {
measurePreTextNode.nodeValue = measureFullText.substr(0, index);
measureSpanTextNode.nodeValue = measureFullText.charAt(index);
measurePostTextNode.nodeValue = measureFullText.substr(index + 1);
return measureSpanEl.offsetHeight != 0
&& measureSpanEl.offsetWidth != 0;
}
/**
* Measures a line break element. Restores any measuring nodes before
* and after operation.
*
* @param {Node}
* lb A line break element to measure.
* @return {Object} A tuple containing the position of the line break in
* the window, and its height.
*/
function measureLineBreak(lb) {
restoreMeasuringNodes();
// Flag this to signify that meaure nodes are occupied.
measurePostTextNode = {};
measureSpanTextNode.nodeValue = _NBSP;
_insertAfter(measureSpanEl, lb);
var spatInf = _getPositionInWindow(measureSpanEl);
spatInf.height = measureSpanEl.offsetHeight;
measureSpanEl.parentNode.removeChild(measureSpanEl);
// Reset flag
measurePostTextNode = null;
return spatInf;
}
/**
* Remeasures a cInfo's position (window and document) at its given
* textnode / charactor, index - and updates the info's properties
* accordingly.
*
* @param {de.cursor.CursorDescriptor}
* cursorDesc
*/
function setSpatialMembers(cursorDesc) {
if (!cursorDesc)
return;
if (_nodeName(cursorDesc.domNode) == "br") {
var inf = measureLineBreak(cursorDesc.domNode);
cursorDesc.x = inf.x;
cursorDesc.y = inf.y;
cursorDesc.height = inf.height;
cursorDesc.width = 0;
} else {
var spatEl;
// Determine which element to get the spatial info from
if (cursorDesc.placement == de.cursor.PlacementFlag.INSIDE) {
setupMeasuringNodes(cursorDesc.domNode);
measureCharactor(cursorDesc.relIndex);
spatEl = measureSpanEl;
} else
spatEl = cursorDesc.domNode;
var pos = _getPositionInWindow(spatEl);
cursorDesc.x = pos.x;
cursorDesc.y = pos.y;
cursorDesc.width = spatEl.offsetWidth;
cursorDesc.height = spatEl.offsetHeight;
}
var docScrollPos = _getDocumentScrollPos();
cursorDesc.docLeft = docScrollPos.left + cursorDesc.x;
cursorDesc.docTop = docScrollPos.top + cursorDesc.y;
restoreMeasuringNodes();
}
// see de.cursor.getNextCursorMovement
function getNextCursorMovement(srcCDesc, left) {
var startNode = srcCDesc.domNode, startIndex = srcCDesc.relIndex, startIsRightOf = srcCDesc.isRightOf, nextNode, nextIndex, nextIsRightOf, placementFlags = getPlacementFlags(srcCDesc.domNode), lastVisitedNode, pendingLineBreak, seenBlockElement = false, prevTextInfo;
if (placementFlags == de.cursor.PlacementFlag.INSIDE) {
// If needs simple flip of is rightof flag then return a flipped
// version
if (srcCDesc.isRightOf == left) {
var cDesc = de.cursor.createCursorDesc(srcCDesc.domNode,
srcCDesc.relIndex, !srcCDesc.isRightOf);
if (cDesc)
return cDesc;
}
} else if (srcCDesc.isRightOf
&& placementFlags != de.cursor.PlacementFlag.INSIDE) {
if (left) {
// If scanning left but source cursor is to the right of an
// element, then need to start the
// traversal within the elements deepest-right descendant
while (startNode.lastChild) {
startNode = startNode.lastChild;
}
if (startNode != srcCDesc.domNode) {
startIndex = _nodeLength(startNode, 2) - 1;
startIsRightOf = true;
}
}
}
// Begin traversing from source point inclusive
_visitAllNodes(
docBody,
startNode,
!left,
function(domNode) {
var firstVisit = domNode == startNode;
// Skip node that are not displayed / is protected
if (!_isNodeDisplayed(domNode)
|| de.doc.isProtectedNode(domNode)) {
lastVisitedNode = domNode;
return true;
}
if (!seenBlockElement && !firstVisit)
seenBlockElement = _isBlockLevel(domNode);
placementFlags = getPlacementFlags(domNode);
// Check after nodes
if (lastVisitedNode) {
var commonAncestor = _getCommonAncestor(domNode,
lastVisitedNode, true);
var checkANodes = _getAncestors(left ? domNode
: lastVisitedNode, commonAncestor, false);
if (left) { // Need to search from top-down in left
// search
checkANodes.reverse();
// Include the current node in the search if it
// is an after node - except for line breaks
if ((placementFlags & de.cursor.PlacementFlag.AFTER)
&& !_isAncestor(domNode,
lastVisitedNode)
&& _nodeName(domNode) != "br")
checkANodes.push(domNode);
} else {
if (!_isAncestor(lastVisitedNode, domNode)
&& _nodeName(lastVisitedNode) != "br"
&& !de.doc
.isProtectedNode(lastVisitedNode)
&& !(lastVisitedNode == startNode && startIsRightOf))
checkANodes.push(lastVisitedNode);
}
for ( var i in checkANodes) {
var node = checkANodes[i];
// Is this node an actual after node?
if (getPlacementFlags(node)
& de.cursor.PlacementFlag.AFTER) {
// Check for pending line break
if (checkLineBreak(node))
return false;
// Found the next move
nextNode = node;
nextIndex = 1;
nextIsRightOf = true;
return false;
}
// Update block level flag
seenBlockElement |= _isBlockLevel(node);
}
}
// Is a dom node which is needing a placeholder?
if (placementFlags == 0) {
if (!de.doc.isNodePackaged(domNode)) {
var phType = 0
if (_doesNeedESPlaceholder(domNode))
phType = 1;
else if (_doesNeedMNPlaceholder(domNode))
phType = 2;
if (phType) {
// Create missing placeholder and add it..
// prevent the undo manager
// from setting the cursor and if there is
// any undo history then
// group this with the last action... and if
// the dontStoreInsertPHOps option is set
// then
// prevent the undo manager from storing the
// operations all together
de.UndoMan
.execute(
de.UndoMan.hasUndo() ? de.UndoMan.ExecFlag.GROUP
: 0,
"InsertHTML",
_getOuterHTML(phType == 1 ? de.doc
.createESPlaceholder(domNode)
: de.doc
.createMNPlaceholder()),
domNode,
domNode.firstChild, 0);
// Check for pending linebreak first
if (!checkLineBreak(domNode.firstChild)) {
// Set next cursor point to the created
// placeholder
nextNode = domNode.firstChild;
nextIndex = 0;
nextIsRightOf = false;
}
return false;
}
}
} else if (placementFlags == de.cursor.PlacementFlag.INSIDE) { // AKA
// A
// text
// node
setupMeasuringNodes(domNode);
var relIndex = firstVisit ? startIndex
: (left ? _nodeLength(domNode) - 1 : 0), isRightOf;
// Determine is rightof flag
if (firstVisit)
isRightOf = startIsRightOf;
else if (prevTextInfo) {
// If measured a text position (start point was
// a text node),
// use its isrightof flag, unless seen a block
// level
// element.
if (seenBlockElement) {
prevTextInfo = null; // Reset to void
// checking for a
// line wrap
isRightOf = left;
} else
isRightOf = prevTextInfo.isRightOf;
} else
isRightOf = left;
// For each charactor in the run of text
for (; (left && relIndex >= 0)
|| (!left && relIndex < measureFullText.length); relIndex += left ? -1
: 1) {
// Measure the charactor. Skip non renderable
// charactors
if (!measureCharactor(relIndex)) {
if (firstVisit)
isRightOf = !isRightOf;
firstVisit = false;
continue;
}
// Determine the charactor position
var inf = _getPositionInWindow(measureSpanEl);
inf.height = measureSpanEl.offsetHeight;
// If there is a pending line break, check to
// see if it
// occurs on the same line as the measured
// renderable charactor
if (checkLineBreak(measureSpanEl, inf))
return false;
// Check if the charactor is at the start or end
// of a line (line wrap start/end)
if (prevTextInfo) {
debug.assert(!firstVisit);
if (!isOnSameLine(inf, prevTextInfo)) {
nextNode = domNode;
nextIndex = relIndex;
nextIsRightOf = !isRightOf;
return false;
}
}
// If this isnt the starting point then found
// the next pos
if (!firstVisit
|| startNode != srcCDesc.domNode) {
nextNode = domNode;
nextIndex = relIndex;
nextIsRightOf = isRightOf;
return false;
}
// Note: no need to check to flip isRightOf flag
// since this is checked before traversal
// Re-setup the measuring nodes (if needs to)
// due to line break measurements above
setupMeasuringNodes(domNode);
firstVisit = false;
// Set prevTextInfo for testing for line wraps
// on next text measure
prevTextInfo = {
domNode : domNode,
isRightOf : isRightOf,
y : inf.y,
height : inf.height
};
// If the node is a placeholder, only count one
// movement
if (de.doc.isMNPlaceHolder(domNode)
|| de.doc.isESPlaceHolder(domNode)) {
prevDesc.isRightOf = false;
break;
}
} // End loop: measuring each char in the run of
// text
// Must restore nodes to avoid traversal going into
// measure nodes themselves
restoreMeasuringNodes();
} else if (placementFlags
& de.cursor.PlacementFlag.BEFORE) {
// Only set as before node if not the first visit or
// if searching left and began to right of the node.
if (!firstVisit
|| (left && domNode == startNode && startIsRightOf)) {
if (!checkLineBreak(domNode)) {
nextNode = domNode;
nextIsRightOf = false;
nextIndex = 0;
}
return false;
}
} else if (_nodeName(domNode) == "br") { // Check for
// line
// breaks
// (Pure
// AFTER
// nodes)
if (!firstVisit) {
// Check for any pending line breaks
if (checkLineBreak(domNode))
return false;
if (left) {
// For LEFT searches check for pending line
// breaks right away since the
// right-node information is always
// available
pendingLineBreak = domNode;
if (checkLineBreak(
lastVisitedNode,
prevTextInfo
&& prevTextInfo.domNode == lastVisitedNode ? prevTextInfo
: null))
return false;
} else { // Right
// Set new pending line break
pendingLineBreak = domNode;
}
}
}
lastVisitedNode = domNode;
// For right searches, if the cursor begins to the right
// on an element then avoid
// traversing inside the descendants
if (!left
&& firstVisit
&& srcCDesc.isRightOf
&& placementFlags != de.cursor.PlacementFlag.INSIDE)
return 1;
}); // End traversal
restoreMeasuringNodes();
// Found a cursor?
return nextNode ? de.cursor.createCursorDesc(nextNode, nextIndex,
nextIsRightOf) : null;
/**
* An inner support function. Determines whether a pending line
* break should be the next cursor position
*
* WARNING: This may restore any current measuring nodes!
*
* @param {Node}
* ele The current element to check pending line breaks
* against
*
* @param {Object}
* eleDesc Optional (created if not provided). A
* descriptor, with at least the y and height members set
*
* @return {Boolean} True iff the next move is a pending line break
* (in which case next node/index will be set).
*
*/
function checkLineBreak(ele, eleDesc) {
// If there is a pending line break, check to see if it
// occurs on the same line as this
if (pendingLineBreak) {
// Measure pending line break and element spatial qaulties
var lbMeas = measureLineBreak(pendingLineBreak), eleDesc = eleDesc
|| (_nodeName(ele) == "br" ? measureLineBreak(ele)
: {
y : _getPositionInWindow(ele).y,
height : ele.offsetHeight
});
if (!isOnSameLine(lbMeas, eleDesc)) {
// If this before after node is not on the same line as
// the pending
// line break, then count the line break as part of the
// move.
nextNode = pendingLineBreak;
nextIndex = 1;
nextIsRightOf = true;
return true;
}
pendingLineBreak = null;
}
return false;
} // End inner checkLineBreak function
} // End getNextCursorMovement
// See de.cursor.getCursorDescAtXY
function getCursorDescAtXY(targetX, targetY, targetNode) {
if (!targetNode)
targetNode = _getRenderedNodeAtXY(targetX, targetY);
// If the target was the cursor - recalc the target to the node
// behind the cursor.
if (targetNode == cursorDiv) {
cursorDiv.style.display = "none";
targetNode = _getRenderedNodeAtXY(targetX, targetY);
cursorDiv.style.display = "";
}
if (!targetNode)
return null;
// Get the searchspace for the bin search
var searchSpace = getBinSearchSpace();
// Perform the dual binary search
var cDesc = searchBestCursorPos(targetX, targetY, searchSpace);
// Restore the DOM structure
restoreMeasuringNodes();
return cDesc;
/**
* An inner supporting function.
*
* @return The search space for the binary search
*/
function getBinSearchSpace() {
// Build array of all nodes to search inside of targetEl
var nodesToSearch = [], totalPlacementLength = 0, wndSize = _getWindowSize();
(function traverse(domNode) {
var placementFlags = getPlacementFlags(domNode), checkElement, posInWindow;
// Avoid adding nodes which are not in the viewport
if (domNode.nodeType == Node.ELEMENT_NODE) {
checkElement = domNode;
} else if (domNode.nodeType == Node.TEXT_NODE
&& placementFlags != 0) {
setupMeasuringNodes(domNode);
measurePreTextNode.nodeValue = "";
measureSpanTextNode.nodeValue = measureFullText;
measurePostTextNode.nodeValue = "";
checkElement = measureSpanEl;
}
if (checkElement) {
posInWindow = _nodeName(checkElement) == "br" ? measureLineBreak(checkElement)
: _getPositionInWindow(checkElement);
// Is this element above the veiw port?
if ((posInWindow.y + checkElement.offsetHeight) <= 0) {
restoreMeasuringNodes();
return true;
}
// Is this element below the veiw port?
if (posInWindow.y > wndSize.height) {
restoreMeasuringNodes();
return false;
}
restoreMeasuringNodes();
}
if (placementFlags == de.cursor.PlacementFlag.INSIDE) { // AKA
// a
// text
// node
nodesToSearch.push({
domNode : domNode,
startIndex : totalPlacementLength,
endIndex : totalPlacementLength
+ _nodeLength(domNode) - 1,
length : _nodeLength(domNode),
placement : de.cursor.PlacementFlag.INSIDE
});
totalPlacementLength += _nodeLength(domNode);
} else if (placementFlags & de.cursor.PlacementFlag.BEFORE) {
nodesToSearch.push({
domNode : domNode,
startIndex : totalPlacementLength,
endIndex : totalPlacementLength + 1,
length : 1,
placement : de.cursor.PlacementFlag.BEFORE, // Only
// store
// before
// flag
posInWnd : posInWindow
// cache this
});
totalPlacementLength++;
}
// Recurse in order traversal
var child = domNode.firstChild;
var continueTrav = true;
while (child) {
if (!traverse(child)) {
continueTrav = false; // Started to traverse in
// node below the viewport
break;
}
child = child.nextSibling;
}
// Add after nodes (event if aborting traversal)
if (placementFlags & de.cursor.PlacementFlag.AFTER) {
nodesToSearch.push({
domNode : domNode,
startIndex : totalPlacementLength,
endIndex : totalPlacementLength + 1,
length : 1,
placement : de.cursor.PlacementFlag.AFTER, // Only
// store
// after
// flag
posInWnd : posInWindow
// cache this
});
totalPlacementLength++;
}
return continueTrav;
})(targetNode);
return {
nodes : nodesToSearch,
totalLength : totalPlacementLength
};
} // End inner getBinSearchSpace
} // End getCursorDescAtXY
/**
* IMPORTANT: Measurement nodes are left un-restored after this
* operation Call restoreMeasuringNodes if you want to restore the dom.
*
* @param {Number}
* targetX The x coord to get the closest cusor pos to
* @param {Number}
* targetY The y coord to get the closest cusor pos to
* @param {Object}
* A dual bin search space to search.
* @param {de.cursor.CursorDescriptor}
* targetLineRef A reference point to search for a best
* position directly above or below the line. Null for full
* search.
*
* @param {Boolean}
* aboveLine If given targetLineRef then set to true to
* search for best position above the reference point. Other
* false will search below the reference point.
*
* @return {de.cursor.CursorDescriptor} The closest cursor position it
* can find. Null if could not find one.
*
*/
function searchBestCursorPos(targetX, targetY, searchSpace,
targetLineRef, aboveLine) {
// Hash table as an associative array, caches measurements
var nonRenderables = {};
if (searchSpace.totalLength == 0)
return null;
// Sample first position in target element
var startDesc = getCursorDescFrom(0, 0, 2);
if (!startDesc)
return null;
// Sample last charactor in target element
var endDesc = getCursorDescFrom(searchSpace.nodes.length - 1,
searchSpace.nodes[searchSpace.nodes.length - 1].length - 1,
1);
debug.assert(endDesc != null);
// Check to see if first and last samples are the same
if (startDesc.domNode == endDesc.domNode
&& startDesc.absIndex == endDesc.absIndex) {
// There must be only 1 renderable charactor in the target
// element
return validDescriptor(startDesc);
}
// Store the first samples
var samples = [ startDesc, endDesc ];
var best = null;
// Determine closest sample and set as the current best
best = isCloserToTarget(startDesc, endDesc, targetY) ? startDesc
: endDesc;
// Upper and lower are the bounds of the search space in the form of
// cursor descriptors
var upper, lower;
// This algorithm has two passes: The first pass is a binary search
// to discover the line, or closest line,
// that the target is on. The second pass is a binary search to home
// in on the closest charactor to the target
// on the line that was found to be the best.
for (var pass = 1; pass <= 2; pass++) { // dual binary search
if (pass == 1) { // setup first pass: Y DOMAIN
if (targetLineRef) {
// If the binary search should find best matching
// position above or below a line reference point,
// then discover all lines within the search space
discoverAllLines(startDesc,
getNodeIndex(startDesc.absIndex), endDesc,
getNodeIndex(endDesc.absIndex));
// Select the closest sample that does not fall on the
// line reference point
selectBest();
// Now the the target line has been discovered, set the
// target Y
targetY = best.y + (best.height / 2);
// Re-calc best abs delta y
best.ady = Math.abs(best.y - targetY);
// Move to X-bin-search
continue;
} else {
// Select the upper and low for the Y range - goto next
// pass if already on target line
if (!selectYRange())
continue;
}
} else { // setup second pass: X DOMAIN
var res = selectXRange();
if (typeof res == "boolean") {
if (!res)
break; // Else the upper and lower range has been
// selected
} else
break; // the best was found
}
// Enter binary search for quickly locating closest line, or
// charactor
while (true) {
debug.assert(lower.absIndex < upper.absIndex);
// If lower is next to upper and was doing the line search,
// then the line search is done... an exact match wasn't
// found
// and the binary search verged towards two charactors side
// by side but on different lines.
// ...and if was doing the charactor search, then the search
// has verged at the final point
if (lower.absIndex == (upper.absIndex - 1))
break;
// Determine current index by halving the search space
var curAbsIndex = lower.absIndex
+ Math.floor((upper.absIndex - lower.absIndex) / 2);
// Ensure that the index is not out of bounds
if (curAbsIndex == lower.absIndex)
curAbsIndex++;
else if (curAbsIndex == upper.absIndex)
curAbsIndex--;
// Locate which node within the target element that the
// current absolute index is in
var curNodeIndex = getNodeIndex(curAbsIndex);
// Get the cursor desc at the current node/rel-index
var current = getCursorDescFrom(curNodeIndex, curAbsIndex
- searchSpace.nodes[curNodeIndex].startIndex, 0);
// Upper and Lower are next to each other
if (!current)
break;
// In the first pass all samples are recorded - the initial
// upper and lower bounds of the next pass
// will be salvaged from these samples.
if (pass == 1)
samples.push(current);
// Check to see if current is the new best
if (isCloserToTarget(current, best, targetY))
best = current;
if (pass == 1) { // Line search
// Check to see if current matches line.
if (targetY >= current.y
&& targetY <= (current.y + current.height))
break; // finished since found a match
// Narrow search
else if (current.y > targetY)
upper = current;
else
lower = current;
} else { // Charactor search
// Determine if current is on the same line as the best
if (isOnSameLine(current, best)) {
// See if target X is on top of current
if (targetX >= current.x
&& targetX <= (current.x + current.width))
break; // If so, then search complete
// Otherwise narrow search based on x position
else if (current.x > targetX)
upper = current;
else
lower = current;
} // If not on same line, then narrow search based on
// Y coordinates
else if (current.y > targetY)
upper = current;
else
lower = current;
}
} // End loop: core binary search for finding line and
// charactor
} // End passes
// FINISHED
return validDescriptor(best);
// Support functions to follow...
/**
* @param {Number}
* absIndex The abs index in the search-space
*
* @return {Number} The index within nodes that absIndex resides
*/
function getNodeIndex(absIndex) {
var nodes = searchSpace.nodes;
// If there is one text node, then clearly the current index is
// that node.
if (nodes.length == 1)
return 0;
// Is it in the first text node?
if (absIndex >= nodes[0].startIndex
&& absIndex <= nodes[0].endIndex)
return 0;
// Is in last text node?
if (absIndex >= nodes[nodes.length - 1].startIndex
&& absIndex <= nodes[nodes.length - 1].endIndex)
return nodes.length - 1;
// begin a little binary search to quickely find the node in the
// search space
var lo = 0;
var up = nodes.length - 1;
var nodeIndex;
while (true) {
var cur = lo + Math.floor((up - lo) / 2);
if (cur == lo)
cur++;
else if (cur == up)
cur--;
if (absIndex >= nodes[cur].startIndex
&& absIndex <= nodes[cur].endIndex) {
nodeIndex = cur;
break;
} else if (absIndex < nodes[cur].startIndex)
up = cur; // search downward
else
lo = cur; // search upward
} // End loop: binary search for locating #text node
return nodeIndex;
} // End inner getNodeIndex
/**
* Gets a cursor descriptor from a given position in the search
* space.
*
* Some text nodes in the search space may not support a cursor
* placement since they can contain only non-renderable symbols.
* Therefore the returned cursor desc may not be at the given
* node/index.
*
* @param {Number}
* dir 0 = Both, within upper and lower bounds, 1 = Left
* only, 2 = Right only.
* @param {Number}
* relIndex The relative index
* @param {Number}
* nodeIndex The index within the search space nodes
*
* @return {Object} the cursor descriptor or NULL if did not find a
* cursor at the given position that is in bounds.
*/
function getCursorDescFrom(nodeIndex, relIndex, dir) {
var nodes = searchSpace.nodes,
searchLeft = dir == 0 || dir == 1,
// Store the current relative/node index
// for restoring when switching scan direction
origialRelIndex = relIndex, originalNodeIndex = nodeIndex,
// Ideally we would directly measure spatial info at the current
// node / relative index.
// However some charactors in text nodes aren't renderable, and
// thus we must scan left and/or right
// to find next renderable charactor
measureEl = null, node; // the node within the searchspace nodes
// Find first renderable symbol. 1 pass for non-text nodes,
// 1-2 pass for text nodes and searching both dirs: 1st pass
// search left, 2nd pass search right.
do {
if (!searchLeft && dir == 0) { // 2nd pass?
// Switching direction...
nodeIndex = originalNodeIndex;
relIndex = origialRelIndex + 1; // exclusive
// Check to see if the right of the starting point is in
// a different node
if (relIndex > nodes[nodeIndex].endIndex) {
nodeIndex++;
relIndex = 0;
// Note: If the nodeIndex is out of bounds, the next
// loop will instantly break
}
}
var reachedSSBounds = false; // refers to search-space
// upper/lower bounds
// Scan through the nodes
while (nodeIndex >= 0 && nodeIndex < nodes.length) {
node = nodes[nodeIndex];
// Ignore non-diplayed nodes
if (_isNodeDisplayed(node.domNode)) {
// Discover the type of node
if (node.placement == de.cursor.PlacementFlag.INSIDE) { // AKA
// Text
// node
// Setup measurement nodes for this text node we
// are about to search in
setupMeasuringNodes(node.domNode);
// Scan through charactors.. looking for the
// first renderable symbol
while (relIndex >= 0
&& relIndex < measureFullText.length) {
// Is the text node/index out of bounds
// (only when scanning both ways)?
if (dir == 0
&& ((lower.domNode == node.domNode && lower.relIndex == relIndex) || (upper.domNode == node.domNode && upper.relIndex == relIndex))) {
reachedSSBounds = true;
break;
}
// or have we measured this before and found
// it was not a renderable char?
if (nonRenderables["_" + nodeIndex + '_'
+ relIndex]) {
relIndex += (searchLeft ? -1 : 1);
continue; // avoid re-measuring
// unrenderable node
}
// Measure the current node / index.
if (!measureCharactor(relIndex)) {
// Char is not renderable, note the
// element and store in the
// hash table (using an accoiative
// array)
nonRenderables["_" + nodeIndex + '_'
+ relIndex] = true;
relIndex += (searchLeft ? -1 : 1);
continue;
}
// Determine position of the charactor. This
// will be used as a flag
// for ending the search for the first
// non-renderable charactor
measureEl = measureSpanEl;
break;
} // End loop: Searching for renderable
// charactor
} else { // BEFORE and AFTER nodes
// Is the text node/index out of bounds (only
// when scanning both ways)?
if (dir == 0
&& ((lower.domNode == node.domNode && lower.placement == node.placement) || (upper.domNode == node.domNode && upper.placement == node.placement))) {
reachedSSBounds = true;
break;
}
measureEl = node.domNode;
break;
}
}
// Have we found the next sample, or has the right-scan
// reached the upper search-space bound?
if (measureEl || reachedSSBounds)
break;
// Setup current node index for scanning the next node
nodeIndex += (searchLeft ? -1 : 1);
// Setup relative index for searching for next
// renderable char
relIndex = (searchLeft && nodeIndex >= 0 && nodeIndex < nodes.length) ? (nodes[nodeIndex].placement == de.cursor.PlacementFlag.INSIDE ? nodes[nodeIndex].length - 1
: 1)
: 0;
} // End loop: scanning nodes in a particular direction
// Was a sample found?
if (measureEl)
break;
} while (dir == 0 && !(searchLeft = !searchLeft)); // End loop:
// scanning
// left
// and/or
// right
// If there were no places where the cursor can be placed
// between lower and upper,
// then the search is done.
if (!measureEl)
return null;
var pos, width, height;
// Is their cached spatial calculations for this node?
if (node.posInWnd)
pos = node.posInWnd;
if (_nodeName(measureEl) == "br") {
pos = pos || measureLineBreak(measureEl);
height = pos.height;
width = 0;
} else {
pos = pos || _getPositionInWindow(measureEl);
width = measureEl.offsetWidth;
height = measureEl.offsetHeight;
}
var adxl = Math.abs(pos.x - targetX), adxr = Math.abs(pos.x
+ width - targetX), isRightOf;
switch (node.placement) { // searchspace placement flags are
// separated (cannot have combined
// flags)
case de.cursor.PlacementFlag.BEFORE:
isRightOf = false;
break;
case de.cursor.PlacementFlag.AFTER:
isRightOf = true;
break;
default: // INSIDE/TEXT
isRightOf = adxr < adxl;
}
return {
domNode : node.domNode,
relIndex : relIndex,
absIndex : node.startIndex + relIndex,
placement : node.placement,
isRightOf : isRightOf,
x : pos.x,
y : pos.y,
adx : isRightOf ? adxr : adxl,
ady : Math.min(Math.abs(pos.y - targetY), Math.abs(pos.y
+ measureEl.offsetHeight - targetY)),
width : width,
height : height
};
} // End inner getCursorDescFrom
/**
* @return {Boolean} True if the range is set. False if the start or
* end sample is on the targets line
*/
function selectYRange() {
// If the start or end sample is not on the target line, and the
// target is within the samples y-range,
// then find the best line
if (!((targetY >= startDesc.y && targetY <= (startDesc.y + startDesc.height)) || (targetY >= endDesc.y && targetY <= (endDesc.y + endDesc.height)))
&& targetY >= startDesc.y
&& targetY <= (endDesc.y + endDesc.height)) {
// If the target Y does not occur on the start or end
// sample's line:
lower = startDesc;
upper = endDesc;
return true;
}
return false; // Otherwise, the line-search is done! Next
// pass...
} // End inner selectYRange
/**
* @return {Boolean, de.cursor.CursorDescriptor} False if the best
* is right at the target. Or True if the range has been
* selected. Or A cursor descriptor of the best match if
* found while setting the range - in which case the best
* var is set
*
*/
function selectXRange() {
// Determine the lower and upper bounds to start the charactor
// binary search with
if (targetX >= best.x && targetX < (best.x + best.width)) {
// If the best is right at the target, then the search is
// done
return false;
} else if (best.x > targetX) { // search to left of best
upper = best;
lower = null;
for (i in samples) {
current = samples[i];
/*
*
* if (current == best) continue;
*
*
* var isLeftOrAbove;
* // Check if this sample (current) is to the left or
* above of best if (isOnSameLine(current, best)) { //
* If sample is on sample line as best, then check X
* coords isLeftOrAbove = current.x < best.x;
* } else isLeftOrAbove = current.y < best.y; // Check
* Y coords if not on same line // If the sample is to
* the left, or above, of best. Then check to see if the
* sample // is closer than the current lower to best.
* if (isLeftOrAbove && (!lower || isCloserToXY(current,
* lower, best.x, best.y)) && current.absIndex <
* upper.absIndex) { lower = current; }
*/
// Select preceeding sample to best in sample set
if (current.absIndex < best.absIndex
&& (!lower || current.absIndex > lower.absIndex))
lower = current;
}
if (!lower) {
// The best line must have been the starting sample,
// thus the best charactor is
// the first renderable charactor.
return best;
}
} else { // Search to the right of best
lower = best;
upper = null;
for (i in samples) {
current = samples[i];
/*
* var isRightOrBelow;
* // Check if this sample (current) is to the right or
* below of best if (isOnSameLine(current, best)) { //
* If sample is on sample line as best, then check X
* coords isRightOrBelow = current.x > best.x;
* } else isRightOrBelow = current.y > best.y; // Check
* Y coords if not on same line // If the sample is to
* the right, or below, of best. Then check to see if
* the sample // is closer than the current upper to
* best. if (isRightOrBelow && (!upper ||
* isCloserToXY(current, upper, (best.x + best.width),
* best.y)) && current.absIndex > lower.absIndex) {
* upper = current; }
*/
if (current.absIndex > best.absIndex
&& (!upper || current.absIndex < upper.absIndex))
upper = current;
}
if (!upper) {
// The best line must have been the ending sample, thus
// the best charactor is
// the first renderable charactor.
return best;
}
}
return true;
} // End inner selectXRange
/**
* Sets the best local for searching for the closest cursor position
* before/after a line reference point.
*/
function selectBest() {
// Begin with the search space bounds... depending on whether
// the search should
// look above or below the line reference point
best = aboveLine ? startDesc : endDesc;
// Look in all samples... which contain all lines
for ( var i in samples) {
var sample = samples[i];
// Skip samples that are either on the same line as the
// reference line, or
// is out of bounds...
if (isOnSameLine(sample, targetLineRef)
|| (aboveLine && sample.y > targetLineRef.y)
|| (!aboveLine && sample.y < targetLineRef.y))
continue;
// Set new best if sample is better
if (isCloserToTarget(sample, best, targetY)) {
best = sample;
}
}
} // End inner selectBest
/**
* Samples the search space to discover all lines.
*
* @param {de.cursor.CursorDescriptor}
* lo A cursor desc
* @param {de.cursor.CursorDescriptor}
* up A cursor desc
*
* @param {Number}
* lni Lower node index in search space
* @param {Number}
* uni Upper node index in search space
*/
function discoverAllLines(lo, lni, up, uni) {
var stack = [ [ lo, lni, up, uni ] ];
while (stack.length > 0) { // simulating recursion - faster and
// avoids stack overflows
var args = stack.pop();
lo = args[0];
lni = args[1];
up = args[2];
uni = args[3];
// Special attention must be payed to tables. They contain
// inner lines.
var ni = lni;
// Shrink lower range if lower is a table
while (ni < searchSpace.nodes.length
&& _nodeName(searchSpace.nodes[ni].domNode) == "table") {
ni++;
}
if (ni == searchSpace.nodes.length)
continue;
if (ni != lni) {
lo = getCursorDescFrom(ni, 0, 2);
lni = ni;
samples.push(lo); // duplicates are ok
}
ni = uni;
// Shrink upper range if upper is a table
while (ni >= 0
&& _nodeName(searchSpace.nodes[ni].domNode) == "table") {
ni--;
}
if (ni == -1)
continue;
if (ni != uni) {
up = getCursorDescFrom(ni,
searchSpace.nodes[ni].length - 1, 1);
uni = ni;
samples.push(up); // duplicates are ok
}
// If lower is directly next to upper or they are on the
// same line
// then the line discovery between these two points is
// complete
if (lo.absIndex >= (up.absIndex - 1)
|| isOnSameLine(up, lo))
continue;
// Determine middle index by halving the search space
var midIndex = lo.absIndex
+ Math.floor((up.absIndex - lo.absIndex) / 2);
// Ensure that the index is not out of bounds
if (midIndex == lo.absIndex)
midIndex++;
else if (midIndex == up.absIndex)
midIndex--;
// Locate which node within the target element that the
// current absolute index is in
var midNodeIndex = getNodeIndex(midIndex);
// Get the cursor desc at the mid point
lower = lo; // getCursorDescFrom uses this for boundry
// checks
upper = up; // getCursorDescFrom uses this for boundry
// checks
var mid = getCursorDescFrom(midNodeIndex, midIndex
- searchSpace.nodes[midNodeIndex].startIndex, 0);
// Upper and Lower are next to each other
if (!mid)
continue;
// Record the sample
samples.push(mid);
// Simulate recursion using a local stack
stack.push([ lo, lni, mid, midNodeIndex ]); // left side
stack.push([ mid, midNodeIndex, up, uni ]); // right side
} // Next
} // End inner discoverAllLines
/**
* Esnures that the given descriptor is valid.
*
* @param {de.cursor.CursorDescriptor}
* cDesc A desciptor
*
* @return If the given descriptor wasn't valid, it returns a
* neighbouring cursor placement, otherwise the given
* descriptor is returned.
*/
function validDescriptor(cDesc) {
if (!cDesc)
return null;
// If the search ended on a line break, then check to make sure
// that the line break has no cursor placements to the left or
// right of it...
if (_nodeName(cDesc.domNode) == "br") {
var prevDesc = getNextCursorMovement(cDesc, true);
if (prevDesc && isOnSameLine(cDesc, prevDesc)) {
cDesc = prevDesc;
cDesc.isRightOf = true;
} else {
var nextDesc = getNextCursorMovement(cDesc, false);
if (nextDesc && isOnSameLine(cDesc, nextDesc)) {
cDesc = nextDesc;
cDesc.isRightOf = false;
}
}
} else if (de.doc.isMNPlaceHolder(cDesc.domNode))
cDesc.isRightOf = false;
cDesc.placement = getPlacementFlags(cDesc.domNode);
// Supply the position of the cursor in the actual document
// rather than just within the window.
var docScrollPos = _getDocumentScrollPos();
cDesc.docLeft = docScrollPos.left + cDesc.x;
cDesc.docTop = docScrollPos.top + cDesc.y;
return cDesc;
} // End inner validDescriptor
} // End searchBestCursorPos
})();
// End Cursor.js
// Start Doc.js
var
/**
* @final The protected node classname prefix
* @type String
*/
_PROTECTED_CLASS = "sw-protect",
/**
* @final The editable section node classname prefix
* @type String
*/
_ES_CLASS_PREFIX = "editable",
/**
* @final The classname use for packaged nodes
* @type String
*/
_PACKAGE_CLASS_NAME = "sw-packaged";
/**
* Note: may want to check _doesNeedESPlaceholder first.
*
* @param {Node}
* domNode A dom node in the document to test.
* @return {Boolean} True if the given dom node is in need of a modifiable
* placeholder.
*
* @see _doesNeedESPlaceholder
*/
function _doesNeedMNPlaceholder(domNode) {
if (_isPlaceholderCandidate(domNode))
return !_doesContainTanglableDescendant(domNode);
return false;
}
/**
* @param {Node}
* domNode A dom node in the document to test.
* @return {Boolean} True if the given dom node is in need of a editable
* section placeholder.
*
* @see _doesNeedMNPlaceholder
*/
function _doesNeedESPlaceholder(domNode) {
if (de.doc.isEditSection(domNode))
return !_doesContainTanglableDescendant(domNode);
return false;
}
/**
* @param {Node}
* domNode A dom node to test
* @return {Boolean} True iff the dom node contains a descendant for which
* the cursor can be placed by.
*/
function _doesContainTanglableDescendant(domNode) {
var containsTangableNode = false;
// Check if needs a placeholder
_visitAllNodes(
domNode,
domNode,
true,
function(node) {
if (node == domNode || de.doc.isProtectedNode(node))
return;
var pflags = de.cursor.getPlacementFlags(node);
if (pflags == de.cursor.PlacementFlag.INSIDE) {
if (_doesTextSupportNonWS(node)
&& _nodeLength(node) > 0) {
if (_isAllWhiteSpace(node.nodeValue)) {
// Validate that the text node is tangable
var measureSpan = $createElement("span"), measureText = document
.createTextNode(node.nodeValue);
measureSpan.appendChild(measureText);
node.nodeValue = "";
node.parentNode.appendChild(measureSpan);
containsTangableNode = measureSpan.offsetHeight != 0
&& measureSpan.offsetWidth != 0;
node.parentNode.removeChild(measureSpan);
node.nodeValue = measureText.nodeValue;
// TODO: Opera sometimes (randomly) incorrectly
// sets the offset width/height to zero
// on text nodes...
} else
containsTangableNode = true;
}
} else if (pflags)
containsTangableNode = true;
return !containsTangableNode;
});
return containsTangableNode;
} // End function doesContainTanglableDescendant
/**
*
* @param {Node}
* node A dom node to test
* @return {Undefined, Boolean} True if node is a placeholder candidate.
* Undefined if it is not.
*/
var _isPlaceholderCandidate = function() {
/*
* A map containing elements the cursor can navigate into which do not
* contain a "Tangle node." Excludes body
*/
var placeholderCandidates = $createLookupMap("li,dd,dt,p,td,th,h1,h2,h3,h4,h5,h6,pre,div");
return function(node) {
return placeholderCandidates[_nodeName(node)] || node == docBody;
}
}();
(function() {
$enqueueInit("Doc", function() {
// Make as subject
_model(de.doc);
// Preprocess the document: consolidate all existing/initial
// editable sections
var es = de.doc.getAllEditSections();
_recordOperations = false;
for ( var i in es) {
_consolidateWSSeqs(es[i], true);
}
_recordOperations = true;
// For dynamically added editable sections, consolidate them too.
de.doc.addObserver({
onSectionAdded : function(editSection) {
// Only consolidate within the editable sections to avoid
// possibilities of consoldiating
// surrounding editable DOM with Undo/Redo history.
_recordOperations = false;
_consolidateWSSeqs(editSection, false);
_recordOperations = true;
}
});
});
var propertySetMap = {}, MN_PH_CLASS = "sw-mn-ph", ES_PH_CLASS = "sw-es-ph", ES_CLASS_TEST_REGEXP = new RegExp(
"^" + _ES_CLASS_PREFIX + ".*$"), ES_CLASS_MATCH_REGEXP = new RegExp(
"^" + _ES_CLASS_PREFIX + "-?(.+)$"), PROTECTED_NODE_TEST_REGEXP = new RegExp(
"^" + _PROTECTED_CLASS + "$"), PACKAGED_NODE_TEST_REGEXP = new RegExp(
"^" + _PACKAGE_CLASS_NAME + "$");
// Create the doc namespace
/**
* @namespace Provides CSS like language for declaring and customizing
* editable sections on web pages.
*
*
*
* Whenever a new editable section has been dynamically added to the
* document, a "onSectionAdded" event is fired, where the argument is
* the added editable section.
*
*
*
* Whenever a editable section has been dynamically removed from the
* document, a "onSectionRemoved" event is fired, where the argument is
* the removed editable section.
*
* @borrows de.mvc.AbstractSubject#addObserver as this.addObserver
*
* @borrows de.mvc.AbstractSubject#removeObserver as this.removeObserver
*
* @author Brook Novak
*/
de.doc = {
/**
* @param {Node}
* node A dom node
*
* @return {Node} The first ancestor element of node which is an
* editable section, inclusive of the given node itself.
* Null if the node / none of its ancestors are editable
* sections.
*/
getEditSectionContainer : function(node) {
return _findAncestor(node, null, this.isEditSection, true);
},
/**
* Determines whether a dom node is marked as edit section element.
*
* @param {Node}
* node The dom node to test
*
* @return {Boolean} True if node is a edit section element
*/
isEditSection : function(node) {
if (node && node.nodeType == Node.ELEMENT_NODE)
return _findClassName(node, ES_CLASS_TEST_REGEXP);
return false;
},
/**
* @return {[Node]} An array of editable sections currently in the
* document.
*/
getAllEditSections : function() {
var editSections = [];
_visitAllNodes(docBody, docBody, true, function(domNode) {
if (de.doc.isEditSection(domNode))
editSections.push(domNode);
});
return editSections;
},
/**
* @param {Node}
* domNode A dom node to test
* @return {Boolean} True if the given dom node is a descendant of
* an editable section.
*/
isNodeEditable : function(domNode) {
var es = this.getEditSectionContainer(domNode);
return es != null && es != domNode;
},
/**
* @param {Node}
* domNode A dom node to test
* @return {Node} The protected nodes container is the node is
* proected. If the dom is a container then it will be
* returned. Otherwise null will be returned.
*/
getProtectedNodeContainer : function(domNode) {
return _findAncestor(domNode, null,
function(node) {
return node.nodeType == Node.ELEMENT_NODE
&& _findClassName(node,
PROTECTED_NODE_TEST_REGEXP);
}, true);
},
/**
* @param {Node}
* domNode A dom node to test
* @return {Boolean} True if the given dom node is, or is an
* descendant of, a protected node
*/
isProtectedNode : function(domNode) {
return this.getProtectedNodeContainer(domNode) != null;
},
/**
* @param {Node}
* domNode A dom node to test
*
* @return {Node} The package root node. Null if the node is not
* packaged.
*/
getPackageContainer : function(domNode) {
return _findAncestor(domNode, null, function(node) {
return node.nodeType == Node.ELEMENT_NODE
&& _findClassName(node, PACKAGED_NODE_TEST_REGEXP);
}, true);
},
/**
* A "packaged" node is part of a tree of nodes which are not
* allowed to be edited, although they are in an editable section.
*
* @param {Node}
* domNode A dom node to test
* @return {Boolean} True if the given dom node is part of a
* package.
*/
isNodePackaged : function(domNode) {
return this.getPackageContainer(domNode) != null;
},
/**
* Declares or overrides a property set for editable sections.
*
* Attributes:
*
* actionFilter: "[!][actionname1[,actionname2[,...]]]" where actual
* name is case insensitive undoable action name. Format action can
* be followed by sub-action encapsulated in brackets
*
* If the property name exists, then the existing set will be
* overridden. You can override the default property set by using
* the name "defaultSet".
*
* @example de.doc.declarePropertySet("metadata", { inputFilter:
* "[[a-z][A-Z][1-9]\\s\\n]*", actionFilter:
* "!blockquote,changecontainer,format(link)" Accept all
* actions EXCEPT for blockquote,changecontainer,formatting
* links
*
* });
*
* @see TODO REFER TO SPEC
*
* @param {String}
* name The name of the class being declared.
*
*
* @param {Object}
* properties A set of attributes that is associated with
* the given name.
*
*/
declarePropertySet : function(name, properties) {
properties = _clone(properties);
// Build action filter
if (typeof properties.actionFilter == "string") {
var actionFilter = properties.actionFilter;
// Is the action inclusive or exclusive?
if (actionFilter.charAt(0) != '!') {
properties.afInclusive = true;
// Add implicit text editing actions
if (actionFilter)
actionFilter += ",";
actionFilter += "inserthtml,inserttext,removedom,removetext";
} else {
actionFilter = actionFilter.substr(1);
properties.afInclusive = false;
}
// Break filter into tokens
var tokens = actionFilter.toLowerCase().split(',');
// Build up the reg exp for quick filtering
var reStr = "(";
for ( var i in tokens) {
reStr += ((i == '0') ? "" : "|");
var token = tokens[i];
var match = /^format\((.+)\)$/.exec(token);
// Format actions can have sub-action filters
if (match) {
var subTokens = match[1].split(',');
for ( var j in subTokens) {
reStr += ((j == '0') ? "" : "|") + "format"
+ subTokens[j];
}
} else {
// Add the action name to the reg exp set
reStr += token;
if (token.indexOf("format") == 0)
reStr += ".+"; // If not sub-format actions are
// defined then declare as all
// format actions
}
} // End loop: parsing action tokens
reStr += ")";
properties.afRE = new RegExp("^" + reStr + "$");
}
// Set or override a property set
propertySetMap[name] = properties;
},
/**
* Declares a batch of property sets in a single call
*
* @param {Object}
* sets An object containing key-value pairs, when the
* keys are property set names, and values are objects
* containing properties.
*/
declarePropertySets : function(sets) {
for ( var tuple in sets) {
this.declarePropertySet(tuple, sets[tuple]);
}
},
/**
* Retreives the editable property set for a given node. This
* considers property inheritance
*
* @param {Node}
* node A dom node
*
* @return {Object} A set of read only dedit atrributes that the
* given node has inherited. Null if the node is not a (or
* descendant of a) editable section.
*/
getEditProperties : function(node) {
// Get the editable section container for this node
if (!this.isEditSection(node))
node = this.getEditSectionContainer(node);
if (node) {
// Get the property set name for this node
var esClassName = _findClassName(node, ES_CLASS_TEST_REGEXP);
if (esClassName) {
var nameMatch = ES_CLASS_MATCH_REGEXP.exec(esClassName);
return nameMatch ? propertySetMap[nameMatch[1]] || {}
: {};
}
return {};
}
return null;
},
/**
* @return {Node} A modifiable node placeholder element.
*
* @see The DOM-based Web Editor Specification 1.0: Section 1.4
*/
createMNPlaceholder : function() {
var ph = $createElement("span");
_setClassName(ph, MN_PH_CLASS);
ph.innerHTML = " ";
return ph;
},
/**
* Determines whether a dom node is (part of) a modifiable node
* placeholder.
*
* @param {Node}
* node A dom node to test
*
* @param {Boolean}
* immediate True to only test if node is a placeholder.
* False to also test if nodes parent is a placeholder.
*
* @return {Boolean} True if node is (part of) a modifiable node
* placeholder. False if it is not
*
* @see The DOM-based Web Editor Specification 1.0: Section 1.4
*/
isMNPlaceHolder : function(node, immediate) {
switch (node.nodeType) {
case Node.ELEMENT_NODE:
return _getClassName(node) == MN_PH_CLASS;
case Node.TEXT_NODE:
return !immediate && node.parentNode
&& _getClassName(node.parentNode) == MN_PH_CLASS;
}
return false;
},
/**
* @param {Node}
* editSection A editible section to create the editable
* section placeholder for
* @return {Node} An editable section placeholder element.
*/
createESPlaceholder : function(editSection) {
var phHTML = this.getEditProperties(editSection).phMarkup
|| " ";
var ph = $createElement("span");
_setClassName(ph, ES_PH_CLASS);
ph.innerHTML = phHTML;
return ph;
},
/**
* Determines whether a dom node is (part of) an editable section
* placeholder.
*
* @param {Node}
* node A dom node to test
*
* @param {Boolean}
* immediate True to only test if node is a placeholder.
* False to also test if the nodes descendants is a
* placeholder.
*
* @return {Boolean} True if node is (part of) a editable section
* placeholder. False if it is not
*/
isESPlaceHolder : function(node, immediate) {
while (node) {
if (node.nodeType == Node.ELEMENT_NODE) {
var clsName = _getClassName(node);
if (clsName == ES_PH_CLASS)
return true;
}
if (immediate)
break;
node = node.parentNode;
}
return false;
},
/**
* Create and adds a new edit section to the document. Sets the
* classname for the given editable section. The is required when
* adding new editable sections after initialization so
* dedit can track its changes. Adds placeholders if they are
* needed.
*
* @param {Node}
* esEle The editable section to register
* @param {String}
* propertySetName The name of the property set to use.
* Can be null/empty for default set.
*
* @see de.doc.removeEditSection For removing a editable section.
*/
registerEditSection : function(esEle, propertySetName) {
// Set class name as editable
var clsName = _getClassName(esEle);
_setClassName(esEle, (clsName ? clsName + " " : "")
+ _ES_CLASS_PREFIX
+ (propertySetName ? "-" + propertySetName : ""));
// Add an editable section placeholder if it needs it
if (!_doesContainTanglableDescendant(esEle))
esEle.appendChild(this.createESPlaceholder(esEle));
this.fireEvent("SectionAdded", esEle); // NB: Changes module
// listens
},
/**
* Unregisters an editable section from the document. Sets the
* classname for the given editable section
*
* @param {Node}
* esEle The edit section to remove from the document.
*/
unregisterEditSection : function(esEle) {
// Strip classname of editable section prefix
var clsName = _getClassName(esEle);
if (clsName)
_setClassName(esEle, clsName.replace(new RegExp("^|\s"
+ _ES_CLASS_PREFIX + "\S*$", "g"), ""))
this.fireEvent("SectionRemoved", esEle); // NB: Changes
// module listens
}
}; // End doc namespace
})();
// End Doc.js
// Start Error.js
var _ErrorMessages = { // TODO: multi-language support ?
'1' : "Bad arguments"
};
/**
* Raises an error and ends execution (throws error)
*
* @param {Number,
* String} arg If string the error raises with contain the
* message otherwise if number then the error will contain the
* message for the error code
*/
function _error(arg) {
throw new Error(typeof arg == "number" ? _ErrorMessages[arg] : arg);
}
// End Error.js
// Start Events.js
/**
* Adds an event handler to a DOM Event Source.
*
* If you are planning to listen for keyboard strokes, you can use
* "keystroke." This uses keydown and/or keypress and provides an extra
* argument to your handler containing the normalized key identifier string.
*
* @param {Object}
* eventSource The Element or Window to add the event to.
*
* @param {String}
* eventName The name of the dom event. Must be lowercase and
* omit the "on" prefix. For example "load" or "keypress".
*
* @param {Function}
* handler A function to handle the event being registered. When
* called, the dom event will be passed as the first (and only)
* argument.
*/
function _addHandler(sourceEle, eventName, handlerFunc) {
debug.assert(sourceEle.nodeType != Node.TEXT_NODE
&& sourceEle.nodeType != Node.COMMENT_NODE);
debug.assert(eventName.indexOf('on') !== 0);
// IE has trouble passing the window object around - it can be cloned
// for no reason
if (sourceEle.setInterval && sourceEle != window)
sourceEle = window;
switch (eventName) {
case "keystroke":
// Keystroke is a combo of keydown and key press - normalizes
// key identifiers and passes them as extra arg to handler
addEventHandler(sourceEle, "keydown", function(e) {
var keyIndent = de.events.Keyboard.getKeyIdentifier(e, true);
if (keyIndent)
return handlerFunc(e, keyIndent);
});
addEventHandler(sourceEle, "keypress", function(e) {
var keyIndent = de.events.Keyboard.getKeyIdentifier(e, false);
if (keyIndent)
return handlerFunc(e, keyIndent);
});
break;
case "mousedown": // Keep mouse state updated
addEventHandler(sourceEle, eventName, function(e) {
return de.events.Mouse.sniffMouseDownEvent(e) ? handlerFunc(e)
: true;
});
break;
case "mouseup": // Keep mouse state updated
addEventHandler(sourceEle, eventName, function(e) {
return de.events.Mouse.sniffMouseUpEvent(e) ? handlerFunc(e)
: true;
});
break;
case "mousemove": // Keep mouse state updated
addEventHandler(sourceEle, eventName, function(e) {
return de.events.Mouse.sniffMouseMoveEvent(e) ? handlerFunc(e)
: true;
});
break;
default:
addEventHandler(sourceEle, eventName, handlerFunc);
}
function addEventHandler(sourceEle, eventName, actualHandler) {
// Keep track of wrapper handlers for removal
// Create event wrapper map (Event type -> tuple[event-source,
// wrapper handler])
if (!handlerFunc.evWrappers)
handlerFunc.evWrappers = {};
// Ensure the map contains an entry for this event type
if (!handlerFunc.evWrappers[eventName])
handlerFunc.evWrappers[eventName] = [];
// Store this event tuple
handlerFunc.evWrappers[eventName].push([ sourceEle, forwardEvent ]);
if (sourceEle.addEventListener)
sourceEle.addEventListener(eventName, forwardEvent, false); // DOM
// Compliant
else if (sourceEle.attachEvent)
sourceEle.attachEvent("on" + eventName, forwardEvent); // IE
// @DEBUG ON
else
debug
.assert(false,
"Unsupported browser: does not support addEventListener or attachEvent");
// @DEBUG OFF
/**
* Wraps an event handler so that the event handler gets a non null
* event and if the handler returns false the event is consumed.
*
* @param {Event}
* e provided by native event dispatcher
*/
function forwardEvent(e) {
de.events.current = e = e || window.event;
try {
if (actualHandler(e) === false)
return de.events.consume(e);
} finally {
de.events.current = 0;
}
}
}
}
/**
* Removes an event handler from a DOM Event Source.
*
* @param {Object}
* eventSource The Element or Window to add the event to.
*
* @param {String}
* eventName The name of the dom event. Must be lowercase and
* omit the "on" prefix. For example "load" or "keypress".
*
* @param {Function}
* handler The handler function to remove form the given event
* source.
*/
function _removeHandler(sourceEle, eventName, handlerFunc) {
debug.assert(eventName.indexOf('on') !== 0);
// Keystoke is comprized of keydown and key press
if (eventName == "keystroke") {
_removeHandler(sourceEle, 'keydown', handlerFunc);
_removeHandler(sourceEle, 'keypress', handlerFunc);
return;
}
// Check if the handler has been wrapped for this event type / source
if (handlerFunc.evWrappers && handlerFunc.evWrappers[eventName]) {
// Look for wrapper handlers for this event type for this handler
for ( var i in handlerFunc.evWrappers[eventName]) {
var tuple = handlerFunc.evWrappers[eventName][i];
// Was there a wrapped handler create for this handler on this
// event type and source?
if (tuple[0] == sourceEle) {
// Remove tuple
if (handlerFunc.evWrappers[eventName].length == 1)
delete handlerFunc.evWrappers[eventName];
else
handlerFunc.evWrappers[eventName].splice(i, 1);
// Assign handlerFunc to be the wrapped handler
handlerFunc = tuple[1];
break;
}
}
}
// Remove the handler from the event source
if (sourceEle.removeEventListener)
sourceEle.removeEventListener(eventName, handlerFunc, false); // DOM
// Compliant
else if (sourceEle.detachEvent)
sourceEle.detachEvent("on" + eventName, handlerFunc); // IE
// @DEBUG ON
else
debug
.assert(false,
"Unsupported browser: does not support addEventListener or attachEvent");
// @DEBUG OFF
}
$extend(de.events, {
/**
* A pointer to the current event
*/
current : 0,
addHandler : _addHandler,
removeHandler : _removeHandler,
/**
* @param {Event}
* e An event
*
* @return {Object} The x and y coordinates of the event relative to the
* window.
*/
getXYInWindowFromEvent : function(e) {
var targetX = 0, targetY = 0;
if (e.clientX || e.clientX === 0) {
targetX = e.clientX;
targetY = e.clientY;
} else if (e.pageX != null) {
var scrollPos = _getDocumentScrollPos();
targetX = e.pageX - scrollPos.left;
targetY = e.pageY - scrollPos.top;
}
return {
x : targetX,
y : targetY
};
},
/**
*
* @param {Event}
* e A dom event
* @return {Node} An element or text node which the event was targetted
* at.
*/
getEventTarget : function(e) {
return e.target || e.srcElement || document;
},
/**
* Consumes an event... stop is from propagating and prevents the
* default action.
*
* @example myhandlers = function(e) { ... if (somecondition) return
* de.events.consume(e); return true; }
*
* @param {Event}
* e A dom event.
*
* @return {Boolean} False always.
*/
consume : function(e) {
de.events.stopPropogation(e);
de.events.preventDefault(e);
return false;
},
/**
* Stops the event from propogating / bubbling.
*
* @param {Event}
* e A dom event.
*/
stopPropogation : function(e) {
if (_engine == _Platform.TRIDENT)
e.cancelBubble = true;
else if (e.stopPropagation)
e.stopPropagation();
},
/**
* Prevents the default behaviour from occuring on a given event.
*
* @param {Event}
* e An event object
*/
preventDefault : function(e) {
if (_engine == _Platform.TRIDENT)
e.returnValue = false;
else if (e.preventDefault)
e.preventDefault();
}
});
// End Events.js
// Start mouse.js
(function() {
var LEFT_BUTTON = "1", RIGHT_BUTTON = "2", toNormalizedIDMap, // Platform
// dependant
// map
// for
// transating
// to
// platform
// independant
// values
draggingScrollBars = false,
// The keys are button values in dom mouse event objects.
// The values are booleans, where true means they are down
mouseStateMap = {};
$enqueueInit("events.Mouse", function() {
// Ensure that the document always captures mouse events to track
// mouse state
// _addHandler(document, "mousedown", function() {}); //
// DEPRECIATED: Selection will always have these events
// _addHandler(document, "mouseup", function() {});
// Setup mouse-button map for translating button/which values into a
// normalized/agreed value for all platforms
if (_browser == _Platform.IE) {
toNormalizedIDMap = {
"1" : LEFT_BUTTON,
"2" : RIGHT_BUTTON
};
} else {// DOM Complient
// DOM have screwed up their specification. There is no way to
// tell
// reliably whether a left click is down since their button
// value
toNormalizedIDMap = {
"0" : LEFT_BUTTON,
"2" : RIGHT_BUTTON
};
}
mouseStateMap[LEFT_BUTTON] = mouseStateMap[RIGHT_BUTTON] = false;
});
/**
* @class A singleton that provides cross-browser/platform mouse-state
* facilties.
* @author Brook Jesse Novak
*/
de.events.Mouse = {
sniffMouseDownEvent : function(e) {
// Update mouse button state
var normalizedClickID = toNormalizedIDMap[e.button];
if (normalizedClickID) {
// Left click + ctrl = right click on macs
if (_os == _Platform.MAC
&& normalizedClickID == LEFT_BUTTON && e.ctrlKey)
normalizedClickID = RIGHT_CLICK;
mouseStateMap[normalizedClickID] = true;
}
// Filter out mouse events on the document when the mouse is on
// the scroll bars
if (isOnScrollBars(e)) {
draggingScrollBars = this.isLeftDown();
return false;
}
return true;
},
sniffMouseUpEvent : function(e) {
// Update mouse button state
var normalizedClickID = toNormalizedIDMap[e.button];
if (normalizedClickID) {
// Left click + ctrl = right click on macs. However- the
// user may have depressed the ctrl key before the
// key up event. Therefore, if this is a mac, and if this
// button up event is a left button, then set both left and
// right
// states to no longer being down.
if (_os == _Platform.MAC
&& normalizedClickID == LEFT_BUTTON)
// Sure this isn't perfect but there is no way to tell -
// and its not the end of the world,
// I don't think the use of two buttons at the same time
// is very useful for text-editors anyway!
this.clearDownStates();
else
mouseStateMap[normalizedClickID] = false;
}
draggingScrollBars = this.isLeftDown();
// Filter out mouse events on the document when the mouse is on
// the scroll bars
return !isOnScrollBars(e);
},
sniffMouseMoveEvent : function(e) {
// Filter out mouse move events within the document (including
// the actual document)
// if the user is scrolling.
return !(this.isLeftDown() && draggingScrollBars);
},
/**
* @return {Boolean} True if the left mouse button is currently
* down.
*
*
* Note: On a mac, if the user left-clicks while ctrl is
* down, this seen a right click gesture for their single button
* mouse design - but also does the same thing for mouses with
* multiple buttons.
*
*/
isLeftDown : function() {
return mouseStateMap[LEFT_BUTTON];
},
/**
* @return {Boolean} True if the right mouse button is currently
* down.
*/
isRightDown : function() {
return mouseStateMap[RIGHT_BUTTON];
},
/**
* Clears the button down states. Useful if mouse module is unable
* to capture/sniff mouse up events in a particulat environment ...
* this provides a way to manually clear mousedown states.
*/
clearDownStates : function() {
mouseStateMap[LEFT_BUTTON] = false;
mouseStateMap[RIGHT_BUTTON] = false;
}
}; // End mouse singleton
/**
* Determines whether mouse event targetted at the document is over
* scroll bars
*
* @param {Object}
* e
* @return {Boolean} Evaluate true if mouse event was on scrollbars
*/
function isOnScrollBars(e) {
var target = de.events.getEventTarget(e);
if (target == window || target == document
|| target == document.documentElement) {
// Opera never raises events on document scrollbars
if (_engine == _Platform.PRESTO)
return;
var pos = de.events.getXYInWindowFromEvent(e), viewportSize = _getViewPortSize();
if (pos.x >= viewportSize.width)
return 1;
return pos.y >= viewportSize.height;
}
}
})();
// End mouse.js
// Start FormatEnvironment.js
/**
* Stores all format environment variables. These are used by the undoable
* "Format" action.
*/
var _formatEnvironment = {};
/**
* Sets a specific format variable in the format environment. Can use to
* create or override-existing formatting profiles which are used by the
* "Format" action.
*
* @param {String}
* name The name of the format type. For example "bold" (case
* insensitive)
* @param {Function}
* evalFunc The evaluation function. TODO: DOCUMENT
* @param {Function}
* wrapperFunc The wrapper function. Must return an inline
* element. TODO DOCUMENT
*/
function _setFormatEnvVar(name, evalFunc, wrapperFunc) {
name = name.toLowerCase();
_formatEnvironment[name + "Eval"] = evalFunc;
_formatEnvironment[name + "Wrapper"] = wrapperFunc;
}
;
// Create the default set of formatting variables
(function() {
/**
* Creates a span with a CSS style
*
* @param {String}
* css The CSS style name
* @param {String}
* val The style value to set
* @return {Node} The created wrapper
*/
function createCSSSpan(css, val) {
var wrapper = $createElement("span");
_setStyle(wrapper, css, val);
return wrapper;
}
(function(envVars) {
for ( var i in envVars) {
_setFormatEnvVar(i, envVars[i][0], envVars[i][1]);
}
})({
bold : [
/* Evaluation function */
function(ele) {
var matches = [];
if (_nodeName(ele) == "b" || _nodeName(ele) == "strong")
matches.push({
type : 1
});
var fw = ele.style.fontWeight;
if (fw) {
var isBold = fw == "bold";
if (!isBold) {
fw = parseInt(fw);
isBold = (!isNaN(fw) && fw >= 700)
}
if (isBold)
matches.push({
type : 3,
match : "fontWeight"
});
}
return matches.length > 0 ? {
strip : matches,
inline : $createElement("strong"), // Extracted inline
// equivalent
value : true
} : null;
},
/* Wrapper */
function() {
return $createElement("strong");
} ],
italics : [
/* Evaluation function */
function(ele) {
var matches = [];
if (_nodeName(ele) == "i" || _nodeName(ele) == "em")
matches.push({
type : 1
});
if (ele.style.fontStyle == "italic")
matches.push({
type : 3,
match : "fontStyle"
});
return matches.length > 0 ? {
strip : matches,
inline : $createElement("em"), // Extracted inline
// equivalent
value : true
} : null;
},
/* Wrapper */
function() {
return $createElement("em");
}
],
underline : [
/* Evaluation function */
function(ele) {
var matches = [];
if (_nodeName(ele) == "u")
matches.push({
type : 1
});
if (ele.style.textDecoration == "underline")
matches.push({
type : 3,
match : "textDecoration"
});
return matches.length > 0 ? {
strip : matches,
inline : createCSSSpan("textDecoration", "underline"), // Extracted
// inline
// equivalent
value : true
} : null;
},
/* Wrapper */
function() {
return createCSSSpan("textDecoration", "underline");
}
],
strike : [
/* Evaluation function */
function(ele) {
var matches = [];
if (_nodeName(ele) == "strike")
matches.push({
type : 1
});
if (ele.style.textDecoration == "line-through")
matches.push({
type : 3,
match : "textDecoration"
});
return matches.length > 0 ? {
strip : matches,
inline : createCSSSpan("textDecoration", "line-through"), // Extracted
// inline
// equivalent
value : true
} : null;
},
/* Wrapper */
function() {
return createCSSSpan("textDecoration", "line-through");
}
],
color : [
/* Evaluation function */
function(ele) {
if (ele.style.color && ele.style.color.length > 0)
return {
strip : [ {
type : 3,
match : "color"
} ],
inline : createCSSSpan("color", ele.style.color),
value : ele.style.color
};
},
/* Wrapper */
function(value) {
return createCSSSpan("color", value);
}
],
backcolor : [
/* Evaluation function */
function(ele) {
if (ele.style.backgroundColor
&& ele.style.backgroundColor.length > 0)
return {
strip : [ {
type : 3,
match : "backgroundColor"
} ],
inline : createCSSSpan("backgroundColor",
ele.style.backgroundColor),
value : ele.style.backgroundColor
};
},
/* Wrapper */
function(value) {
return createCSSSpan("backgroundColor", value);
}
],
fontsize : [
/* Evaluation function */
function(ele) {
if (_nodeName(ele) == "small" || _nodeName(ele) == "big") {
var val = _nodeName(ele) == "small" ? "smaller" : "larger";
return {
strip : [ {
type : 1
} ],
inline : createCSSSpan("fontSize", val),
value : val
};
}
if (ele.style.fontSize && ele.style.fontSize.length > 0)
return {
strip : [ {
type : 3,
match : "fontSize"
} ],
inline : createCSSSpan("fontSize", ele.style.fontSize),
value : ele.style.fontSize
};
},
/* Wrapper */
function(value) {
return createCSSSpan("fontSize", value);
} ],
fontfamily : [
/* Evaluation function */
function(ele) {
if (ele.style.fontFamily
&& ele.style.fontFamily.length > 0)
return {
strip : [ {
type : 3,
match : "fontFamily"
} ],
inline : createCSSSpan("fontFamily",
ele.style.fontFamily),
value : ele.style.fontFamily
};
},
/* Wrapper */
function(value) {
return createCSSSpan("fontFamily", value);
} ],
link : [
/* Evaluation function */
function(ele) {
if (_nodeName(ele) == "a")
return {
strip : [ {
type : 1
} ],
inline : ele.cloneNode(false),
value : {
url : ele.href,
title : ele.title
}
};
},
/* Wrapper */
function(value) {
var wrapper = $createElement("a");
wrapper.href = value.url;
wrapper.title = value.title;
return wrapper;
} ]
});
})();
// End FormatEnvironment.js
// Start Fragment.js
/**
* @class
*
* DOMFragment's are used to describe a range of DOM nodes. They are similar
* W3C DOM Ranges, except they have a more rich/expressive range and contain
* information for reversing operations safely.
*
* @constructor
* @private
*
* Use de.DOMFragment.buildFragment
*
* @param {Node}
* domNode The dom node to wrap
* @param {Number}
* posInParent The position of the domNode in is parent child
* list.
*
*/
var _DOMFragment = function() {
var cls = function(domNode, posInParent) {
/**
* The wrapped dom node
*
* @type Node
*/
this.node = domNode;
/**
* A read only member: The position of the domNode in is parent
* child list.
*
* @type Number
*/
this.pos = posInParent;
/**
* A read only member: The de.dom.DOMFragment's children. Never
* null.
*
* @type [de.dom.DOMFragment]
*/
this.children = [];
/**
* True indicates that this node has descendants (including self)
* that are outside of the fragment range. Flase indicates that the
* node is completely within the fragments range.
*
* @type Boolean
*/
this.isShared = false;
/**
* The dom fragments parent. Null for root.
*
* @type de.dom.DOMFragment
*/
this.parent = null;
}
cls.prototype = {
/**
* Applies a function to nodes in order starting from this node and
* all its children.
*
* @param {Function}
* func A function applied to the nodes. 1 argument is
* given: a visited node. The traversing will be stopped
* by returning false.
*/
visit : function(func) {
if (func(this) === false)
return false;
for ( var i in this.children) {
if (!this.children[i].visit(func))
return false;
}
return true;
},
/**
*
* Removes the fragment from the document.
*
*/
disconnect : function() {
this.visit(function(currentFrag) {
if (!currentFrag.isShared)
removeFragment(currentFrag, false);
});
},
/**
* An extension of de.dom.DOMFragment#disconnect. This routine
* furthermore collapses nodes after the disconnection: where nodes
* the have been left in the document -- within the fragment range --
* are removed/copied/moved in a way that a typical word-proccessor
* would behave.
*
* The fragment must not be disconnected prior to this call.
*
* @return {Node} The first migrated node. Null if nothing was
* migrated.
*/
collapse : function() {
// Get the first common ancestor between the start and end
// fragments... this may not be the root.
var firstCommonAncestor = _getCommonAncestor(this
.getStartFragment().node, this.getEndFragment().node,
false), fcaFrag = this, stopMigratingSiblings = false, // used
// for
// inner
// function
fragRoot = this, // used for inner function
hasCollapsibleBoundAncestor = false; // used for inner
// function
while (fcaFrag.node != firstCommonAncestor) {
fcaFrag = fcaFrag.children[0];
}
// Remove the fragments inclusive range from the document
fragRoot.disconnect();
// Insert any placeholders that needs placing
insertPlaceholders();
// Remove all migratable nodes and build migration trees
var migrations = migratePath(fcaFrag, true,
nextMigrationPoint(fragRoot.getStartFragment())), i, j;
// Add migration trees into their migration points
for (i in migrations) {
if (migrations[i].migrantRoots.length > 0) {
// Discover the insertion index in the migration point
var mPointNode = migrations[i].migrationPoint.node, insertIndex = migrations[i].migrationPoint.children[0].pos, // Start-bound's
// index
containsNonPHTangable = false, seenPlaceholder = 0;
// If the start-bound child is still in the document,
// then increase the index
if (isInDocument(migrations[i].migrationPoint.children[0].node))
insertIndex++;
// Insert the migrants
for (j = 0; j < migrations[i].migrantRoots.length; j++) {
_execOp(_Operation.INSERT_NODE,
migrations[i].migrantRoots[j], mPointNode,
insertIndex + j);
}
// Check to see if migration point has any redundant
// placeholders, which may have been uneccessarely
// added,
// or just moved, via the collapse operation.
// First check if there are any tangable descendants of
// the migration point which are not
// immediate children that are placeholders...
if (_isPlaceholderCandidate(mPointNode)) {
_visitAllNodes(
mPointNode,
mPointNode,
true,
function(node) {
if (node == mPointNode)
return;
if (!(de.doc.isMNPlaceHolder(node,
false) && (node.nodeType == Node.TEXT_NODE ? node.parentNode.parentNode == mPointNode
: node.parentNode == mPointNode))
&& de.cursor
.getPlacementFlags(node)) {
containsNonPHTangable = true;
return false;
}
});
}
// Scan through the migration points immediate nodes
for (var k = 0; k < mPointNode.childNodes.length; k++) {
var domNode = mPointNode.childNodes[k];
if (de.doc.isMNPlaceHolder(domNode, true)
|| de.doc.isESPlaceHolder(domNode, true)) {
// If we have already seen a placeholder in this
// migration point, or the migration point is
// not
// a placeholder candidate, or the migration
// point contains tangable nodes that are not
// immediate
// children who are placeholders... then this
// placeholder is redundant
if (seenPlaceholder
|| !_isPlaceholderCandidate(mPointNode)
|| containsNonPHTangable)
_execOp(_Operation.REMOVE_NODE, domNode); // Remove
// it
// from
// the
// document
seenPlaceholder = 1;
}
}
}
}
// Return the first migrated node
return migrations.length > 0
&& migrations[0].migrantRoots.length > 0 ? migrations[0].migrantRoots[0]
: null;
/**
* Removes/clones nodes from the document and creates migration
* trees -- paired with their migration points, for which they
* should be appended to.
*
* @param {de.dom.DOMFragment}
* pathRoot The top-most fragment of the path to
* travel up to.
* @param {Object}
* isEndBoundPath True iff the path is the
* end-boundry path
* @param {de.dom.DOMFragment}
* curMigPoint The current migration point
* @return {[Object]} The migrations to make
*/
function migratePath(pathRoot, isEndBoundPath, curMigPoint) {
var curMigration = {
/*
* A migration point is a dom node in the start-bound
* path where nodes in the end bound path can
* move(migrate) to.
*/
migrationPoint : curMigPoint,
/*
* Migration roots are disconnected dom-trees which
* should be connected with the migration point in this
* instance by adding as children ... which is done
* outside of this function scope.
*/
migrantRoots : []
}
var migrations = [ curMigration ];
// Follow a path starting from the end node and moving up to
// the path's root
for (var fragment = isEndBoundPath ? pathRoot
.getEndFragment() : pathRoot.getStartFragment(); fragment != (isEndBoundPath ? pathRoot
: pathRoot.parent); fragment = fragment.parent) {
var domNode, nextNode;
// Check to see if this fragment has an ancestor who is
// collapsible and is on the end-bound path.
if (isEndBoundPath) {
hasCollapsibleBoundAncestor = false;
for (var f = fragment.parent; f != pathRoot; f = f.parent) {
if (f.isShared && isCollapsible(f.node)) {
hasCollapsibleBoundAncestor = true;
break;
}
}
}
if (!isEndBoundPath || fragment.isShared) {
domNode = fragment.node;
nextNode = domNode.nextSibling;
// Should this node be removed? I.E: Is the node
// collapsable?
if (isCollapsible(domNode)) {
stopMigratingSiblings = true;
// All descendants of a collapsiable node on the
// bound path should be migrated
debug
.assert(!isEndBoundPath
|| (isEndBoundPath && !domNode.firstChild));
// Remove this collapsible node if it is on the
// end-bound path and is shared
if (isEndBoundPath)
_execOp(_Operation.REMOVE_NODE,
fragment.node);
}
// Check to see if this node should be migrated, or
// should stay
if (!isEndBoundPath || canDirectlyMigrate(domNode)) {
// Check if need to remove this migrant or
// duplicate it
if (domNode.firstChild)
domNode = domNode.cloneNode(false);
else
_execOp(_Operation.REMOVE_NODE, domNode);
// Is the current migration point valid?
while (curMigration.migrationPoint != fcaFrag
&& !_isValidRelationship(
domNode,
curMigration.migrationPoint.node)
&& _nodeName(domNode) != "div") {
// One exception: don't allow migration of
// div element: the generic container
// Search for next valid migration point...
curMigration.migrationPoint = nextMigrationPoint(curMigration.migrationPoint);
}
// Add this node to all migrations
for (var i = 0; i < migrations.length; i++) {
// Link up dom node to its children
for ( var j in migrations[i].migrantRoots) {
_execOp(_Operation.INSERT_NODE,
migrations[i].migrantRoots[j],
domNode);
}
// Set the new roots for this tree-level
migrations[i].migrantRoots = [ domNode ];
// Duplicate node for next migration point
if (i < (migrations.length - 1))
domNode = domNode.cloneNode(false);
}
} else if (!isCollapsible(domNode)) { // Fragment
// in
// boundry
// path that
// cannot be
// migrated
stopMigratingSiblings = true;
if (!domNode.firstChild)
// The un-migratible node has been left
// childless, remove it from the document
_execOp(_Operation.REMOVE_NODE,
fragment.node);
}
} else { // Is on end bound path and fragment not
// shared and has been disconnected. i.e.
// completely removed from document
// Determine next node...
// If the parent has been disconnected, then there
// is no use searching through the next siblings
if (!fragment.parent.isShared)
continue;
// See if this fragments parent is part of the
// starting bound
var startBound = fragRoot;
while (startBound.children.length > 0
&& startBound != fragment.parent) {
startBound = startBound.children[0];
}
if (startBound == fragment.parent) {
debug.assert(startBound.children.length > 0);
// If the start bound path shares the same
// parent for the current fragment
// (which is on the end path), then the next
// sibling is not neccesarily the first
// remaining child in this fragment's parent.
nextNode = null;
if (!startBound.insertedPH) { // Watch out for
// these, since
// they are
// added on the
// fly, not in
// fragment
// structure
if (isInDocument(startBound.children[0].node)) {
// The first child of the start bound is
// in the document
nextNode = startBound.node.childNodes.length > (startBound.children[0].pos + 1) ? startBound.node.childNodes[startBound.children[0].pos + 1]
: null;
} else {
// The first child of the start bound
// has been removed
nextNode = startBound.node.childNodes.length > startBound.children[0].pos ? startBound.node.childNodes[startBound.children[0].pos]
: null;
}
}
// Get the first remainding child in this
// disconnected fragment's parent
} else
nextNode = fragment.parent.node.firstChild; // May
// be
// null/not
// exist
}
// If the parent cannot be migrated from the end-bounds
// path, then don't migrate it's children to
// the left of the end-bounds.
if (isEndBoundPath
&& !canDirectlyMigrate(fragment.parent.node)
&& !isCollapsible(fragment.parent.node))
stopMigratingSiblings = true;
// If this fragment is for the starting iteration of a
// new migrant fragment, then avoid recursing into
// it's sibling dom nodes that are not in a fragment
// yet... this will be handled afterwards
else if (!isEndBoundPath && fragment == pathRoot)
continue;
// Get this fragments child-index in it's parent
var nextFragIndex = fragment.getIndexInParent();
// Set/reset the migration index for this fragments
// parent. Only applicable on the bound path
if (isEndBoundPath)
fragment.parent.migrantIndex = 0;
while (nextNode
&& (hasCollapsibleBoundAncestor || !stopMigratingSiblings)) { // For
// each
// adjacent
// path
domNode = nextNode; // For bound path, valid for
// non-bound path
nextNode = domNode.nextSibling; // For bound path,
// valid for
// non-bound path
nextFragIndex++; // For non-bound path
var adjacentMigrations;
if (isEndBoundPath) {
var migrantFragRoot;
// If this operation is being repeated, then get
// the migration tree created from the first
// time
// this operation was performed
if (fragment.parent.migrants
&& fragment.parent.migrants.length > fragment.parent.migrantIndex) {
migrantFragRoot = fragment.parent.migrants[fragment.parent.migrantIndex];
} else { // First time this operation has
// been performed
// Build a fragment to capture the structure
// of the current document's state
migrantFragRoot = _buildFragment(
domNode.parentNode, domNode, 0,
domNode, _nodeLength(domNode, 1));
// Link it to the parent fragment
if (!fragment.parent.migrants)
fragment.parent.migrants = [];
fragment.parent.migrants
.push(migrantFragRoot);
}
fragment.parent.migrantIndex++;
adjacentMigrations = migratePath(
migrantFragRoot.children[0],
false,
migrations[migrations.length - 1].migrationPoint); // Recurse
} else { // Fragment already created...
adjacentMigrations = migratePath(
fragment.parent.children[nextFragIndex],
false,
migrations[migrations.length - 1].migrationPoint); // Recurse
}
// Merge the adjacent migrations into the current
// migrations
for ( var i in adjacentMigrations) {
if (adjacentMigrations[i].migrantRoots.length == 0)
continue;
// Search for a matching migration point
var wasMerged = false;
for ( var j in migrations) {
if (migrations[j].migrationPoint == adjacentMigrations[i].migrationPoint) {
// Append the adjacent migrant roots for
// this migration point (at the current
// tree-level)
migrations[j].migrantRoots = migrations[j].migrantRoots
.concat(adjacentMigrations[i].migrantRoots);
wasMerged = true;
break;
}
}
// Must be a new migration point... add to the
// current set of migration points
if (!wasMerged)
migrations.push(adjacentMigrations[i]);
}
// Keep the current migration updated
curMigration = migrations[migrations.length - 1]; // TODO:
// NEEDED?
} // End loop: recursing over adjacent paths
} // End loop: travelling up path to root
return migrations;
} // End inner migratePath
/**
* Looks up the start-bound path from the given migration point.
*
* @param {_Fragment}
* currentPoint Must not be the first Common Ancestor -
* must be on the start bound path.
* @return {_Fragment} The next migration point
*/
function nextMigrationPoint(currentPoint) {
debug.assert(currentPoint != fcaFrag);
do {
currentPoint = currentPoint.parent;
} while (currentPoint != fcaFrag && !(
// currentPoint.isShared && // NOTE: Cannot used isShared to
// determine if exists in document... since cna be
// re-inserted
isInDocument(currentPoint.node) && /*
* i.e: shared nodes or
* re-inserted nodes due
* to placeholders
*/
isCollapsible(currentPoint.node)));
return currentPoint;
} // End inner nextMigrationPoint
/**
* @param {Node}
* domNode
* @return {Boolean} True if domNode is classed as "collapsible"
*/
function isCollapsible(domNode) {
return _isGenericBlockLevel(domNode)
|| _nodeName(domNode) == "li";
} // End inner isCollapsible
/**
* @param {Node}
* domNode
* @return {Boolean} True if domNode can directly be migrated
* from the boundry path into a migration point
*/
function canDirectlyMigrate(domNode) {
return domNode.nodeType == Node.TEXT_NODE
|| _isInlineLevel(domNode);
} // End inner canDirectlyMigrate
function isInDocument(domNode) {
return _isAncestor(docBody, domNode);
}
/**
* If the start-bound path contains any placeholder candidates
* either left without tangable nodes or are fully disconnected,
* then placeholders are inserted / fragments are reconnected
*/
function insertPlaceholders() {
for (var fragment = fragRoot.getStartFragment(); fragment != null; fragment = fragment.parent ? fragment.parent
: null) {
var domRef = fragment.node;
if (_isPlaceholderCandidate(domRef)
|| de.doc.isEditSection(domRef)) {
// Is this still in the document?
if (fragment.isShared) {
var phType = 0
if (_doesNeedESPlaceholder(domRef))
phType = 1;
else if (_doesNeedMNPlaceholder(domRef))
phType = 2;
if (phType) {
// The disconnection has left a placeholder
// candidate in the document without a
// tangable node.
// Create a new placeholder and add it
var ph = phType == 1 ? de.doc
.createESPlaceholder(domRef)
: de.doc.createMNPlaceholder();
_execOp(_Operation.INSERT_NODE, ph, domRef);
// Mark that this fragment inserted a
// placeholder
fragment.insertedPH = 1;
}
} else { // disconnected
// Get rid of any children that were part of the
// diconnection
while (domRef.firstChild) {
_execOp(_Operation.REMOVE_NODE,
domRef.firstChild);
}
// Create a new placeholder and add it
_execOp(_Operation.INSERT_NODE, de.doc
.createMNPlaceholder(), domRef);
// Mark that this fragment inserted a
// placeholder
fragment.insertedPH = 1;
// Link this and all it's disconnect ancestors
// back into the document...
while (!fragment.isShared) {
if (fragment.parent.isShared) {
// Reconnect this with it's parent
_execOp(_Operation.INSERT_NODE,
fragment.node,
fragment.parent.node,
fragment.pos);
} else {
// Make sure the parent has all its
// children removed
var parentDomNode = fragment.parent.node;
while (parentDomNode.firstChild) {
_execOp(_Operation.REMOVE_NODE,
parentDomNode.firstChild);
}
// Reconnect this with it's parent
_execOp(_Operation.INSERT_NODE,
fragment.node, parentDomNode);
}
if (!fragment.parent)
break;
fragment = fragment.parent;
}
}
break; // No need to search ancestors for inserting
// placeholders as this point
}
}
}
},
/**
* @return {de.dom.DOMFragment} The start fragment of the fragment's
* range.
*/
getStartFragment : function() {
var startFrag = this;
while (startFrag.children.length > 0) {
startFrag = startFrag.children[0];
}
return startFrag;
},
/**
* @return {de.dom.DOMFragment} The end fragment of the fragment's
* range.
*/
getEndFragment : function() {
var endFrag = this;
while (endFrag.children.length > 0) {
endFrag = endFrag.children[endFrag.children.length - 1];
}
return endFrag;
},
/**
* @return {Integer} The zero-based index of this fragment in it's
* parent. Null if this is a root fragment.
*/
getIndexInParent : function() {
if (!this.parent)
return null;
var index = 0;
while (this != this.parent.children[index]) {
index++;
}
return index;
},
/**
* @return {Boolean} True if the start node was a split text node,
* so that it has a preceeding text node which was the
* orginal node, false otherwise.
*/
wasStartSplit : function() {
return this.getStartFragment().preSplitNode ? true : false;
},
/**
* @return {Boolean} True if the end node was a split text node, so
* that it has a proceeding text node which was the new
* split node, false otherwise.
*/
wasEndSplit : function() {
return this.getEndFragment().postSplitNode ? true : false;
},
/**
* @return {Node} The previous text node which will split at the
* start fragment. Null if this framgent didn't split the
* start point
*/
getPreSplitNode : function() {
var startFrag = this.getStartFragment();
return startFrag.preSplitNode ? startFrag.preSplitNode : null;
},
/**
* @return {Node} The following text node which will split at the
* end fragment. Null if this framgent didn't split the end
* point
*/
getPostSplitNode : function() {
var endFrag = this.getEndFragment();
return endFrag.postSplitNode ? endFrag.postSplitNode : null;
},
/**
* Gets the adjusted node/index of the given tuple to point to a
* valid position in the DOM which they may have been left pointing
* to invalid indexes due to the construction of this fragment.
*
* @param {Object}
* node
*
* @param {Object}
* index
*
*/
getAdjustedNodeIndex : function(node, index) {
// Check if node belongs in the formatting fragments' split text
// nodes, and see if it's index is out of bounds
if (node.nodeType == Node.TEXT_NODE
&& index >= _nodeLength(node)) {
var startFrag = this.getStartFragment(), endFrag = this
.getEndFragment();
// Was the start node split? And did the node previously
// point to this split node?
if (startFrag.wasStartSplit()) {
var preSplitNode = startFrag.getPreSplitNode();
if (node == preSplitNode) {
index -= _nodeLength(preSplitNode);
node = startFrag.node;
}
}
// Was the end node split? And did the node previously point
// to the split node?
if (endFrag.wasEndSplit() && node == endFrag.node
&& index >= _nodeLength(node)) {
index -= _nodeLength(endFrag.node);
node = endFrag.getPostSplitNode();
}
}
return {
node : node,
index : index
};
},
/**
* Gets the original node/index values before this fragment was
* built.
*
* @param {Object}
* node
* @param {Object}
* index
*/
getOriginalNodeIndex : function(node, index) {
if (node.nodeType == Node.TEXT_NODE) {
var startFrag = this.getStartFragment(), endFrag = this
.getEndFragment();
// Was the end node split? And does the node point to the
// added split node?
if (node == endFrag.getPostSplitNode()) {
index += _nodeLength(endFrag.node);
node = endFrag.node;
}
// Was the start node split? And does the node point to the
// added split node?
if (startFrag.wasStartSplit() && node == startFrag.node) {
var preSplitNode = startFrag.getPreSplitNode();
index += _nodeLength(preSplitNode);
node = preSplitNode;
}
}
return {
node : node,
index : index
};
}
};
// @DEBUG ON
// Add tostring method for debugging
cls.prototype.toString = function() {
return _nodeName(this.node)
+ (this.isShared ? "[SHARED]" : "[NOT-SHARED]");
}
// @DEBUG OFF
/**
* Removes the fragments dom node from the document.
*
* @param {de.dom.DOMFragment}
* fragment
* @param {Boolean}
* forceRemoveDoc Set to true to always remove the fragments
* node from the document. Set to false to only remove if the
* fragment has a shared parent.
*/
function removeFragment(fragment, forceRemoveDoc) {
if (forceRemoveDoc || (fragment.parent && fragment.parent.isShared))
_execOp(_Operation.REMOVE_NODE, fragment.node);
}
return cls;
}();
/**
* Builds a fragment.
*
* If startNode / endNode are the same, then startIndex / endIndex must be
* different. If startNode or endNodes are placeholders, they are extended
* to include the whole placeholder in the range.
* The start node/index tuple must occur before the end node/index tuple
* when traversing inorder.
* The range will always been extended to the deepest descendants.
*
* @param {Node}
* commonAncestor (Optional) A common ancestor of the start and
* end nodes, can be as high as possible. If null then the
* closest common ancestor will be used.
*
* @param {Node}
* startNode The starting dom node of the fragments range.
*
* @param {Number}
* startIndex The inclusive start index in the start node. Ranges
* from 0 to the text length for text nodes. Where 0 indicates
* that the range begins at the first char, and text length
* indicates that the range begins directly after the text node,
* but not including it.
* Ranges from 0 to 1 for elements. Where 0 indicates that the
* range includes the element and its decendants, and 1 indicates
* that the range excludes the element and its decendants.
*
*
* @param {Node}
* endNode The ending dom node of the fragments range.
*
* @param {Number}
* endIndex The inclusive end index in the end node. Ranges from
* 0 to the text length for text nodes. Where 0 indicates that
* the range ends just before the text node, but not including
* it, and text length indicates that the range ends at the last
* charactor in the text run.
* Ranges from 0 to 1 for elements. Where 0 indicates that the
* range excludes the element and its decendants, and 1 indicates
* that the range includes the element and its decendants.
*
* @return {de.dom.DOMFragment} The fragments root FragmentNode, which will
* always be the commonAncestor. Never null.
*/
function _buildFragment(commonAncestor, startNode, startIndex, endNode,
endIndex) {
debug.assert(startNode != endNode || startIndex < endIndex,
"Invalid range");
debug
.assert(
!(startNode.nodeType == Node.TEXT_NODE && (startIndex < 0 || startIndex > _nodeLength(startNode))),
"Start index out of range");
debug
.assert(
!(startNode.nodeType == Node.ELEMENT_NODE && (startIndex < 0 || startIndex > 1)),
"Start index out of range");
debug
.assert(
!(endNode.nodeType == Node.TEXT_NODE && (endIndex < 0 || endIndex > _nodeLength(endNode))),
"End index out of range");
debug
.assert(
!(endNode.nodeType == Node.ELEMENT_NODE && (endIndex < 0 || endIndex > 1)),
"End index out of range");
debug.assert(!(endNode == startNode && endIndex == startIndex),
"Invalid range");
// Set common ancestor if not given
if (!commonAncestor)
commonAncestor = _getCommonAncestor(startNode, endNode);
// Make sure the range extends to the deepest descendants
var adjustBoundry = false;
while (startNode.firstChild) {
startNode = startIndex == 0 ? startNode.firstChild
: startNode.lastChild;
adjustBoundry = true;
}
if (adjustBoundry && startIndex > 0)
startIndex = _nodeLength(startNode, 1);
adjustBoundry = false;
while (endNode.firstChild) {
endNode = endIndex == 0 ? endNode.firstChild : endNode.lastChild;
adjustBoundry = true;
}
if (adjustBoundry && endIndex > 0)
endIndex = _nodeLength(endNode, 1);
// Make sure placeholders are fully included within the inclusive range
if (de.doc.isMNPlaceHolder(startNode)
|| de.doc.isESPlaceHolder(startNode))
startIndex = 0;
if (de.doc.isMNPlaceHolder(endNode) || de.doc.isESPlaceHolder(endNode))
endIndex = _nodeLength(endNode, 1);
// @DEBUG ON
// Check that end occurs after the start
var foundEnd = false;
_visitAllNodes(commonAncestor, startNode, true, function(domNode) {
foundEnd = domNode == endNode;
return !foundEnd;
});
debug.assert(foundEnd,
"Invalid range: end node not found after start node");
// @DEBUG OFF
var affectedNodes = [];
// Get all the nodes between the start and end nodes.
_visitAllNodes(commonAncestor, startNode, true, function(node) {
affectedNodes.push(node);
return node != endNode;
});
// Add the missing nodes to the start of the array
var ancestors = _getAncestors(startNode, commonAncestor, false, true);
ancestors.reverse();
affectedNodes = ancestors.concat(affectedNodes);
// to traverse the nodes inorder from the common ancestor through
// the start and end nodes range, just iterate forwards on the array:
var fragRoot;
for ( var i in affectedNodes) {
// Build up a fragment by traversing through the affected nodes in
// order
var node = affectedNodes[i], preSplitNode = 0, postSplitNode = 0;
// Perform split operations on boundry text nodes
if ((node == startNode || node == endNode)
&& node.nodeType == Node.TEXT_NODE) {
// Because the first/last nodes are text nodes, we may have to
// split them up.
if (node == startNode && startIndex > 0
&& startIndex < _nodeLength(node)) {
preSplitNode = node;
// Leave the start node's stable references as is, since the
// remainding text
// node within the range is a newly created text node...
node = _execOp(_Operation.SPLIT_TEXT_NODE, node, startIndex);
// Update endnode if range is all within same node
if (endNode == startNode) {
endNode = node;
endIndex -= _nodeLength(startNode);
}
startNode = node;
// No need to worry about start index
}
if (node == endNode && endIndex > 0
&& endIndex < _nodeLength(node)) {
postSplitNode = _execOp(_Operation.SPLIT_TEXT_NODE, node,
endIndex);
}
}
// Create a new fragment node
var fragment = new _DOMFragment(node, _indexInParent(node));
// Has the root been set?
if (!fragRoot)
fragRoot = fragment;
else { // Set up the parent relationship...
fragment.parent = null;
fragRoot.visit(function(frag) {
if (frag.node == fragment.node.parentNode)
fragment.parent = frag;
return fragment.parent == null;
});
fragment.parent.children.push(fragment);
}
// If the node is a boundry node and is excluded, then set a flag to
// note that such nodes are to be outside of the fragment range.
fragment.isShared = !(preSplitNode || postSplitNode)
&& ((node == startNode && startIndex == _nodeLength(node, 1)) || (node == endNode && endIndex == 0));
if (preSplitNode)
fragment.preSplitNode = preSplitNode;
if (postSplitNode)
fragment.postSplitNode = postSplitNode;
} // End iterating over affected nodes
// Determine which nodes are shared with the fragment's range and
// document and which aren't
markSharedNodes(fragRoot);
return fragRoot;
/**
* An inner support function. Sets the "isShared" flag for a fragment
* and all it's descendant fragments
*
* @param {de.dom.DOMFragment}
* currentFrag The fragment to mark from
*/
function markSharedNodes(currentFrag) {
var isAnyChildShared = false;
// Scan children first (disconnecting post order)
for ( var ch in currentFrag.children) {
var childFrag = currentFrag.children[ch];
// Recurse
markSharedNodes(childFrag);
// Detect if any children of the current node are shared
isAnyChildShared |= childFrag.isShared;
}
// Determine if this fragment is shared. The root is a special
// case... thus explicitly must
// be declared as being shared. isShared will already be true if the
// fragment is a boundry
// node (start/end) that has been excluded from the range.
currentFrag.isShared |= (isAnyChildShared
|| currentFrag == fragRoot || currentFrag.node.childNodes.length != currentFrag.children.length);
} // End inner markSharedNodes
}
; // End buildFragment
// Expose build fragment
de.buildFragment = _buildFragment;
// End Fragment.js
// Start OperationManager.js
/* Read Only. Stores all undoable/redoable operation logic */
var _operationRepository = {},
/* Read Only. Stores the current list of operations. Null/undefined if there is none */
_curOperationList,
/* Set to true to record operations, false to ignore. Defaults to true. */
_recordOperations = true;
/**
*
* @param {Number} opCode The unique operation code which identifies the operation.
*
* @param {Function} exec The execution function. The first argument will be the operation data.
* The following arguments are specific execution arguemnts to the operation.
*
* @param {Function} undo The undo function. Given one argument: the operation data.
*
* @param {Function} redo The redo function. Given one argument: the operation data.
*
* @return {Number} The operation code of the registered operation
*/
function _registerOperation(opCode, exec, undo, redo) {
// Should generate operation code?
if (!opCode) {
opCode = _registerOperation.genOp + 1;
do {
opCode++;
} while(_operationRepository[opCode]);
_registerOperation.genOp = opCode;
}
debug.assert(!_operationRepository[opCode], "Attempted to override operation with op code: " + opCode);
_operationRepository[opCode] = {exec:exec,undo:undo,redo:redo};
return opCode;
}
_registerOperation.genOp = 100;
/**
* Executes an undoable operation. If _recordOperations is on then the operation will be stored
* in the _curOperationList list.
*
* When ever dom is to be manipulated, it is always done here. The additional arguments after the given op code
* are specific to the operation.
*
* @param {Number} opCode The operation code of the operation to execute.
*/
function _execOp(opCode) {
// Get the operation code
var operation = _operationRepository[opCode];
debug.assert(operation != null, "Unknown operation: " + opCode);
// Create argument list... begin with the operation data object
var args = Array.prototype.slice.call(arguments);
args.shift();
args.unshift({opCode:opCode});
// Execute the operation
var opRes = operation.exec.apply(operation, args);
// Store the operation data
if (_recordOperations) {
// Create a new operation list if not appending
if (!_curOperationList)
_curOperationList = [];
_curOperationList.push(args[0]);
}
// Return the operatoin-specific result
return opRes;
}
/**
* Note: Wipes current operation list.
* @return {[Object]} The current list of operations. Null if there are none.
*/
function _getOperations() {
var ops = _curOperationList;
_curOperationList = null;
return ops;
}
/**
* Undoes a list of operations. Assumes that the effected DOM state is the same as it was after the operations were executed.
*
* @param {[Object]} opList The list of operations to undo
*/
function _undoOperations(opList) {
for (var i = opList.length - 1; i >= 0; i--) {
var opData = opList[i];
var operation = _operationRepository[opData.opCode];
debug.assert(operation != null, "Unknown operation: " + opData.opCode);
operation.undo(opData);
}
}
/**
* Redoes a list of operations. Assumes that the effected DOM state is the same as it was after the operations were undone.
*
* @param {[Object]} opList The list of operations to redo
*/
function _redoOperations(opList) {
for (var i in opList) {
var opData = opList[i];
var operation = _operationRepository[opData.opCode];
debug.assert(operation != null, "Unknown operation: " + opData.opCode);
operation.redo(opData);
}
}
/**
* Controls whether undoable operations should be recorded.
* ONLY USE IF YOU KNOW WHAT YOU ARE DOING.
*
* TODO: Detailed doc.
*
* @param {Boolean} on True to turn on operation recording. False to turn off.
*/
de.recordOperations = function(on) {
_recordOperations = on;
};
/* BASE OPERATIONS */
// @DEBUG ON
_Operation = {
// @REPLACE _Operation.INSERT_NODE 1
INSERT_NODE : 1,
// @REPLACE _Operation.REMOVE_NODE 2
REMOVE_NODE : 2,
// @REPLACE _Operation.SPLIT_TEXT_NODE 3
SPLIT_TEXT_NODE : 3,
// @REPLACE _Operation.INSERT_TEXT 4
INSERT_TEXT : 4,
// @REPLACE _Operation.REMOVE_TEXT 5
REMOVE_TEXT : 5,
// @REPLACE _Operation.SET_CSS_STYLE 6
SET_CSS_STYLE : 6,
// @REPLACE _Operation.SET_CLASS 7
SET_CLASS : 7,
// @REPLACE _Operation.INSERT_ROW 8
INSERT_ROW : 8,
// @REPLACE _Operation.INSERT_CELL 9
INSERT_CELL : 9,
// @REPLACE _Operation.DELETE_ROW 10
DELETE_ROW : 10,
// @REPLACE _Operation.DELETE_CELL 11
DELETE_CELL : 11
};
// @DEBUG OFF
_registerOperation(_Operation.INSERT_NODE,
/**
* Execute
* @param {Object} data The operation data
* @param {Node} newNode The new dom node to insert
* @param {Node} parent The parent of the dom node to insert into
* @param {Number} index The index in the parent to insert the dom node. Omit to append
*/
function(data, newNode, parent, index){
data.newNode = newNode;
data.parent = parent;
if (index || index === 0) // Is it an append operation?
data.pos = index;
this.redo(data);
},
/* Undo */
function(data){
data.parent.removeChild(data.newNode);
},
/* Redo */
function(data){
if (data.pos || data.pos === 0)
_insertAt(data.parent, data.newNode, data.pos);
else data.parent.appendChild(data.newNode);
}
);
_registerOperation(_Operation.REMOVE_NODE,
/**
* Execute
* @param {Object} data The operation data
* @param {Node} target The Node to remove
*/
function(data, target){
data.parent = target.parentNode;
data.pos = _indexInParent(target);
data.target = target;
this.redo(data);
},
/* Undo */
function(data){
_insertAt(data.parent, data.target, data.pos);
},
/* Redo */
function(data){
data.parent.removeChild(data.target);
}
);
_registerOperation(_Operation.SPLIT_TEXT_NODE,
/* Execute */
function(data, target, index){
data.target = target;
data.index = index;
data.rem = target.splitText(index);
return data.rem;
},
/* Undo */
function(data){
// Restore target nodes full text value
data.target.nodeValue += data.rem.nodeValue;
// Get rid of the split text node
data.rem.parentNode.removeChild(data.rem);
data.rem.nodeValue = ""; // free some memory
},
/* Redo */
function(data){
var fullText = data.target.nodeValue;
// Re-set the splitted nodes text
data.rem.nodeValue = fullText.substr(data.index);
data.target.nodeValue = fullText.substr(0, data.index);
// Re-insert the split node
_insertAfter(data.rem, data.target);
}
);
_registerOperation(_Operation.INSERT_TEXT,
/* Execute */
function(data, target, text, index){
data.target = target;
data.index = index;
data.len = text.length;
var pre = target.nodeValue.substr(0, index),
post = target.nodeValue.substr(index);
target.nodeValue = pre + text + post;
},
/* Undo */
function(data){
data.text = data.target.nodeValue.substr(data.index, data.len);
var pre = data.target.nodeValue.substr(0, data.index),
post = data.target.nodeValue.substr(data.index + data.len);
data.target.nodeValue = pre + post
delete data["len"]; // Free some memory
},
/* Redo */
function(data){
data.len = data.text.length;
var pre = data.target.nodeValue.substr(0, data.index),
post = data.target.nodeValue.substr(data.index);
data.target.nodeValue = pre + data.text + post;
delete data["text"]; // Free some memory
}
);
_registerOperation(_Operation.REMOVE_TEXT,
/* Execute */
function(data, target, index, length){
data.target = target;
data.index = index;
data.text = target.nodeValue.substr(index, length);
var pre = target.nodeValue.substr(0, index),
post = target.nodeValue.substr(index + length);
target.nodeValue = pre + post
},
/* Undo - same as insert text's undo */
_operationRepository[_Operation.INSERT_TEXT].redo,
/* Redp - same as insert text's undo */
_operationRepository[_Operation.INSERT_TEXT].undo
);
_registerOperation(_Operation.SET_CSS_STYLE,
/**
* Execute
* @param {Object} data The operation data
* @param {Node} target The target element to set CSS
* @param {String} css The CSS Field to set in javascript notation
* @param {String} value The value of the CSS to set
*/
function(data, target, css, value){
data.target = target;
data.css = css;
data.newValue = value;
data.oldValue = _engine == _Platform.TRIDENT ? data.target.style.getAttribute(data.css) : data.target.style[data.css];
this.redo(data);
},
/* Undo */
function(data){
if (_engine == _Platform.TRIDENT)
data.target.style.setAttribute(data.css, data.oldValue);
else
data.target.style[data.css] = data.oldValue;
},
/* Redo */
function(data){
if (_engine == _Platform.TRIDENT)
data.target.style.setAttribute(data.css, data.newValue);
else
data.target.style[data.css] = data.newValue;
}
);
_registerOperation(_Operation.SET_CLASS,
/**
* Execute
* @param {Object} data The operation data
* @param {Node} target The target element to set CSS
* @param {String} name The class name to set (replaces full class name)
*/
function(data, target, name){
data.target = target;
data.newName = name;
data.oldName = _getClassName(target);
this.redo(data);
},
/* Undo */
function(data){
_setClassName(data.target, data.oldName);
},
/* Redo */
function(data){
_setClassName(data.target, data.newName);
}
);
_registerOperation(_Operation.INSERT_CELL,
/**
* Execute
* @param {Object} data The operation data
* @param {Node} row The target row element to insert the cell into
* @param {Number} index The index in the row to insert into. Clamped if out of bounds
* @return {Node} The new cell that was created
*/
function(data, row, index){
data.row = row;
data.index = index > row.cells.length ? row.cells.length : index; // Safely clamp range
if (data.index < 0) data.index = 0;
return this.redo(data);
},
/* Undo */
function(data){
data.row.deleteCell(data.index);
},
/* Redo */
function(data){
return data.row.insertCell(data.index);
}
);
_registerOperation(_Operation.INSERT_ROW,
/**
* Execute
* @param {Object} data The operation data
* @param {Node} table The target table element to insert the row into
* @param {Number} index The index in the row to insert into. Clamped if out of bounds
* @return {Node} The new row that was created
*/
function(data, table, index){
data.table = table;
data.index = index > table.rows.length ? table.rows.length : index; // Safely clamp range
if (data.index < 0) data.index = 0;
return this.redo(data);
},
/* Undo */
function(data){
data.table.deleteRow(data.index);
},
/* Redo */
function(data){
return data.table.insertRow(data.index);
}
);
_registerOperation(_Operation.DELETE_ROW,
/**
* Execute
* @param {Object} data The operation data
* @param {Node} table The target table element to insert the row into
* @param {Number} index The index in the row to delete. Clamped if out of bounds
*/
function(data, table, index){
data.table = table;
data.index = index >= table.rows.length ? table.rows.length-1 : index; // Safely clamp range
if (data.index < 0) data.index = 0;
this.redo(data);
},
/* Undo */
function(data){
var newRow = data.table.insertRow(data.index);
// Migrate contents from old removed row into new row
while(data.row.firstChild) {
var migrant = data.row.firstChild;
data.row.removeChild(migrant);
newRow.appendChild(migrant);
}
// Save memory - get rid of old removed row reference
delete data["row"];
},
/* Redo */
function(data){
data.row = data.table.rows[data.index]; // Save row - need to keep contents
data.table.deleteRow(data.index);
}
);
_registerOperation(_Operation.DELETE_CELL,
/**
* Execute
* @param {Object} data The operation data
* @param {Node} row The target row element to delete the cell from
* @param {Number} index The cell index in the row to delete. Clamped if out of bounds
*/
function(data, row, index){
data.row = row;
data.index = index >= row.cells.length ? row.cells.length-1 : index; // Safely clamp range
if (data.index < 0) data.index = 0;
this.redo(data);
},
/* Undo */
function(data){
var newCell = data.row.insertCell(data.index);
// Migrate contents from old removed row into new row
while(data.cell.firstChild) {
var migrant = data.cell.firstChild;
data.cell.removeChild(migrant);
newCell.appendChild(migrant);
}
// Save memory - get rid of old removed row reference
delete data["cell"];
},
/* Redo */
function(data){
data.cell = data.row.cells[data.index]; // Save cell - need to keep contents
data.row.deleteCell(data.index);
}
);
//End OperationManager.js
//Start Keyboard.js
(function() {
// All the keymaps
var keymaps = {
// maps the charcodes of special printable keys to key identifiers
specialToCharCode: {
8: "Backspace", // The Backspace (Back) key.
9: "Tab", // The Horizontal Tabulation (Tab) key.
// Note: This key identifier is also used for the
// Return (Macintosh numpad) key.
13: "Enter", // The Enter key.
27: "Escape", // The Escape (Esc) key.
32: "Space" // The Space (Spacebar) key.
},
// maps the keycodes of non printable keys to key identifiers
keyCodeToId: {
16: "Shift", // The Shift key.
17: "Control", // The Control (Ctrl) key.
18: "Alt", // The Alt (Menu) key.
20: "CapsLock", // The CapsLock key
224: "Meta", // The Meta key. (Apple Meta and Windows key)
37: "Left", // The Left Arrow key.
38: "Up", // The Up Arrow key.
39: "Right", // The Right Arrow key.
40: "Down", // The Down Arrow key.
33: "PageUp", // The Page Up key.
34: "PageDown", // The Page Down (Next) key.
35: "End", // The End key.
36: "Home", // The Home key.
45: "Insert", // The Insert (Ins) key. (Does not fire in Opera/Win)
46: "Delete", // The Delete (Del) Key.
112: "F1", // The F1 key.
113: "F2", // The F2 key.
114: "F3", // The F3 key.
115: "F4", // The F4 key.
116: "F5", // The F5 key.
117: "F6", // The F6 key.
118: "F7", // The F7 key.
119: "F8", // The F8 key.
120: "F9", // The F9 key.
121: "F10", // The F10 key.
122: "F11", // The F11 key.
123: "F12", // The F12 key.
144: "NumLock", // The Num Lock key.
44: "PrintScreen", // The Print Screen (PrintScrn, SnapShot) key.
145: "Scroll", // The scroll lock key
19: "Pause", // The pause/break key
91: "Win", // The Windows Logo key
93: "Apps" // The Application key (Windows Context Menu)
},
// maps the keycodes of the numpad keys to the right charcodes
numpadToCharCode: {
96: "0".charCodeAt(0),
97: "1".charCodeAt(0),
98: "2".charCodeAt(0),
99: "3".charCodeAt(0),
100: "4".charCodeAt(0),
101: "5".charCodeAt(0),
102: "6".charCodeAt(0),
103: "7".charCodeAt(0),
104: "8".charCodeAt(0),
105: "9".charCodeAt(0),
106: "*".charCodeAt(0),
107: "+".charCodeAt(0),
109: "-".charCodeAt(0),
110: ".".charCodeAt(0),
111: "/".charCodeAt(0)
},
// Helpers
charCodeA: "A".charCodeAt(0),
charCodeZ: "Z".charCodeAt(0),
charCodea: "a".charCodeAt(0),
charCodez: "z".charCodeAt(0),
charCode0: "0".charCodeAt(0),
charCode9: "9".charCodeAt(0),
// Platform dependant maps
keyCodeFix : {},
charCodeToKeyCode : {},
// Maps keycodes that cannot be distinguished in key press events, to the other keycodes that have also
// been assigned to the same key in the key press event. ALl key codes (keys/values) in the map will be
// simulated as key presses in the key down event.
ambiguousKeyPressCodes : {}
};
// Construct inverse maps
keymaps.idToKeyCode = {};
for (var key in keymaps.keyCodeToId) {
keymaps.idToKeyCode[keymaps.keyCodeToId[key]] = parseInt(key, 10);
}
for (var key in keymaps.specialToCharCode) {
keymaps.idToKeyCode[keymaps.specialToCharCode[key]] = parseInt(key, 10);
}
// Setup platform dependant key maps
switch (_engine) {
case _Platform.TRIDENT: // MSHTML
keymaps.charCodeToKeyCode = {
13: 13,
27: 27
};
break;
case _Platform.GECKO:
keymaps.keyCodeFix = {
12: idToKeyCode("NumLock")
};
break;
case _Platform.WEBKIT:
// starting with Safari 3.1 (version 525.13) Apple switched the key
// handling to match the IE behaviour.
if (_Platform.engineVersion && _Platform.engineVersion < 525.13) { // TODO: Check if safari?
keymaps.charCodeToKeyCode = {
// Safari/Webkit Mappings
63289: idToKeyCode("NumLock"),
63276: idToKeyCode("PageUp"),
63277: idToKeyCode("PageDown"),
63275: idToKeyCode("End"),
63273: idToKeyCode("Home"),
63234: idToKeyCode("Left"),
63232: idToKeyCode("Up"),
63235: idToKeyCode("Right"),
63233: idToKeyCode("Down"),
63272: idToKeyCode("Delete"),
63302: idToKeyCode("Insert"),
63236: idToKeyCode("F1"),
63237: idToKeyCode("F2"),
63238: idToKeyCode("F3"),
63239: idToKeyCode("F4"),
63240: idToKeyCode("F5"),
63241: idToKeyCode("F6"),
63242: idToKeyCode("F7"),
63243: idToKeyCode("F8"),
63244: idToKeyCode("F9"),
63245: idToKeyCode("F10"),
63246: idToKeyCode("F11"),
63247: idToKeyCode("F12"),
63248: idToKeyCode("PrintScreen"),
3: idToKeyCode("Enter"),
12: idToKeyCode("NumLock"),
13: idToKeyCode("Enter")
};
} else { // Modern versions of webkit
keymaps.charCodeToKeyCode = {
13: 13,
27: 27
};
}
break;
case _Platform.PRESTO:
keymaps.ambiguousKeyPressCodes = {
35: 51, // # <=> End
36: 52, // $ <=> Home
44: 188, // Comma <=> Print Screen
45: 109, // - <=> Insert
46: 190, // Period <=> Delete
91: 219, // [ <=> Windows
93: 221 // ] <=> Apps
};
// Add presto specific maps...
// Inverse ambigious key codes to simulate a keypress for in a key down event,
// True values indicate they only should be simulated if the shift modifier is down.
// False values are implicit (the remainding ambiguios codes) and should only be simulated
// if the shift modifier is not down.
keymaps.prestoSimulateInvOnShift = {
51: 1, // #
52: 1 // $
// 188 : 0 // ,
// 109 : 0 // -
// 190 : 0 // period
// 219 : 0 // [
// 221 : 0 // ]
};
// The "which" codes to use (as well as alpha numeric codes) as char codes on key presses.
keymaps.prestoUseWhichCodes = {
33: 1, // ! <=> Page Up
34: 1, // " <=> Page Down
40: 1, // ( <=> Down
39: 1, // ' <=> Right
38: 1, // & <=> Up
37: 1, // % <=> Left
123: 1 // { <=> F12
};
break;
}
// Create inverse maps
keymaps.ambiguousKeyPressCodesInv = {};
for (var key in keymaps.ambiguousKeyPressCodes) {
keymaps.ambiguousKeyPressCodesInv[keymaps.ambiguousKeyPressCodes[key]] = parseInt(key, 10);
}
/**
* @class A singleton that provides cross-browser/platform keyboard-normalization facilities
* @author Brook Novak
*/
de.events.Keyboard = {
/**
* Returns the event's "normalizedKey" to the DOM Level 3 Spec of keyIdentifier.
* @param {Event} domEvent The dom event to "normalize"
*
* @param {Boolean} isKeyDown True if the dom event for keydown, false if for keypress.
*
* @return {String} The key identifier for the given key.
* Null if the key ident is unknown or should be extracted from a different event type
*/
getKeyIdentifier : function(domEvent, isKeyDown) {
var keyCode, // non printable keys. e.g. CTRL, INSERT
charCode, // printable symbols. e.g. A,Z,&
useGenericMapping = false;
//debug.println((isKeyDown ? "Keydown" : "KeyPress") + ": cc=" + domEvent.charCode + "kc=" + domEvent.keyCode);
switch(_engine) {
case _Platform.TRIDENT:
if (isKeyDown && (isNonPrintableKeyCode(domEvent.keyCode) || domEvent.keyCode == 8 || domEvent.keyCode == 9))
keyCode = domEvent.keyCode;
// Use keydown if CTRL is down, but keyPress if CTRL is not down
if ((!isKeyDown && !domEvent.ctrlKey) || (isKeyDown && domEvent.ctrlKey)) {
if (keymaps.charCodeToKeyCode[domEvent.keyCode])
keyCode = keymaps.charCodeToKeyCode[domEvent.keyCode];
else
charCode = domEvent.keyCode;
}
break;
case _Platform.GECKO:
if (isKeyDown) {
// Moz doesn't get keypress events for CTRL, ALT or SHIFT,
// So raise them on key down
if (domEvent.keyCode >= 16 && domEvent.keyCode <= 18)
keyCode = domEvent.keyCode;
} else {
keyCode = keymaps.keyCodeFix[domEvent.keyCode] || domEvent.keyCode;
charCode = domEvent.charCode;
}
break;
case _Platform.WEBKIT:
if (_browser == _Platform.SAFARI) {
if (isKeyDown) {
if (_engineVersion && _engineVersion < 525.13)
keyCode = keymaps.charCodeToKeyCode[domEvent.charCode] || domEvent.keyCode;
else keyCode = domEvent.keyCode;
if (!isNonPrintableKeyCode(keyCode) && !this.isAcceleratorDown(domEvent))
keyCode = 0;
} else { // Key Press get printable charactors
// starting with Safari 3.1 (verion 525.13) Apple switched the key
// handling to match the IE behaviour.
if (_engineVersion && _engineVersion < 525.13) {
if (keymaps.charCodeToKeyCode[domEvent.charCode])
keyCode = keymaps.charCodeToKeyCode[domEvent.charCode];
else
charCode = domEvent.charCode;
} else {
if (keymaps.charCodeToKeyCode[domEvent.keyCode])
keyCode = keymaps.charCodeToKeyCode[domEvent.keyCode];
else
charCode = domEvent.keyCode;
}
}
} else if(_browser == _Platform.CHROME) {
// Chrome is good, it sets keycode,charcode,which and keyidentifier..
if (isKeyDown) {
// Keycodes can be detected from keydowns
if (isNonPrintableKeyCode(domEvent.keyCode))
keyCode = domEvent.keyCode;
// If the accelerator key is down while pressing printable keys the charcodes
// appear as key codes in the keydown event
else if (this.isAcceleratorDown(domEvent))
charCode = domEvent.keyCode;
} else {
// Printable keys (charcodes) occur in the keypress event (except when
// the accelerator is down).
if (domEvent.charCode && !this.isAcceleratorDown(domEvent))
charCode = domEvent.charCode;
}
} else useGenericMapping = true;
break;
case _Platform.PRESTO:
if (isKeyDown) {
if (keymaps.ambiguousKeyPressCodesInv[domEvent.keyCode]) {
// Avoid simulating key codes which will have a following legit keypress event
var simOnShift = keymaps.prestoSimulateInvOnShift[domEvent.keyCode];
if ((domEvent.shiftKey && simOnShift) || (!domEvent.shiftKey && !simOnShift))
charCode = keymaps.ambiguousKeyPressCodesInv[domEvent.keyCode];
} else if (keymaps.ambiguousKeyPressCodes[domEvent.keyCode])
keyCode = domEvent.keyCode;
} else { // Key press
if (domEvent.which && (isAlphaNumericAscii(domEvent.which) || keymaps.prestoUseWhichCodes[domEvent.which]))
charCode = domEvent.which;
else if (keymaps.keyCodeToId[domEvent.keyCode])
keyCode = domEvent.keyCode;
else
charCode = domEvent.keyCode;
}
break;
case _Platform.KHTML:
// TODO
useGenericMapping = true;
break;
default:
useGenericMapping = true;
} // End switch
if (useGenericMapping && isKeyDown) {
if (domEvent.keyIdentifier && domEvent.keyIdentifier.length > 0) {
// See http://www.w3.org/TR/DOM-Level-3-Events/keyset.html
// Is the key identifier unicode-encoded?
var ucMatch = /^U\+([\dA-Fa-f]+)$/.exec(domEvent.keyIdentifier);
if (ucMatch) {
// Extract unicode numerical value
var uniNum = parseInt(ucMatch[1], 16);
// Convert into printable symbol
var printable = String.fromCharCode(uniNum);
if (domEvent.shiftKey)
return printable.toUpperCase();
else return printable.toLowerCase();
} else return domEvent.keyIdentifier;
} else {
keyCode = domEvent.keyCode || domEvent.which;
charCode = domEvent.charCode;
}
}
if (keyCode) { // Use keyCode
// Omit ambiguous key codes: where actual key presses cannot be distinguished since this
// platform assigns some single key codes to multiple keys.
if (!isKeyDown && keymaps.ambiguousKeyPressCodes[keyCode])
return null;
return keyCodeToId(keyCode);
} else if (charCode) // Use charCode
return charCodeToId(charCode);
return null;
},
/**
* @param {Event} domEvent A dom event
* @return {Boolean} True if the accelerator key was down for the given event.s
*/
isAcceleratorDown : function(domEvent) {
// Add platform specific accelerator flag
return _os == _Platform.MAC ? domEvent.metaKey : domEvent.ctrlKey;
}
}; // End Keyboard singleton
/**
* @param {Number} keyCode A key code to test
* @return {Boolean} True if the key code is non-printable. I.E. not a printable symbol.
*/
function isNonPrintableKeyCode(keyCode) {
return typeof keymaps.keyCodeToId[keyCode] == "string" || typeof keymaps.specialToCharCode[keyCode] == "string";
}
/**
* @param {Number} keyCode a keycode to test
* @return {Boolean} True if the given keycode is identifiable.
*/
function isIdentifiableKeyCode(keyCode) {
return isAlphaNumericAscii(keyCode) ||
keymaps.specialToCharCode[keyCode] || /* Enter, Space, Tab, Backspace */
keymaps.numpadToCharCode[keyCode] || /* Numpad */
isNonPrintableKeyCode(keyCode); /* non printable keys */
}
/**
* @param {Number} code A char-code (or which-code) to test
* @return {Boolean} True if the given code is alphanumeric
*/
function isAlphaNumericAscii(code) {
return (code >= keymaps.charCodeA && code <= keymaps.charCodeZ) || /* Upper */
(code >= keymaps.charCodea && code <= keymaps.charCodez) || /* Lower */
(code >= keymaps.charCode0 && code <= keymaps.charCode9); /* Numbers */
}
/**
* @param {Number} charCode A charactor code
* @return {String} A key identifier for the given charactor code. Undefined if none exists.
*/
function charCodeToId(charCode) {
return keymaps.specialToCharCode[charCode] || String.fromCharCode(charCode);
}
/**
* @param {Number} keyCode A keycode
* @return {String} A key identifier for the given key code. Undefined if none exists.
*/
function keyCodeToId(keyCode) {
if (isIdentifiableKeyCode(keyCode)) {
var numPadCharCode = keymaps.numpadToCharCode[keyCode];
if (numPadCharCode)
return String.fromCharCode(numPadCharCode);
return (keymaps.keyCodeToId[keyCode] || keymaps.specialToCharCode[keyCode] || String.fromCharCode(keyCode));
} else return null;
}
/**
* @param {String} keyId The key identifier string to get the keycode for
* @return {Number} The standadized keycode of the given key identifier
*/
function idToKeyCode(keyId) {
return keymaps.idToKeyCode[keyId] || keyId.charCodeAt(0);
}
}) ();
// End Keyboard.js
//Start Selection.js
var _toggleSectionHighlight;
(function(){
/*
* The selection model
*
* Users can select anything in the document, even GUI's etc. The selection is not native, but is a emulated
* model which works on all browsers. The reason while it is emulated is because when the cursor module manipulates
* the dom around the clicked nodes - the native selection models fall over and the selection goes haywire on every platform.
*
* Usually in a typical content editor the cursor follows the end-of-selection. However this is confusing for users
* when a blinking cursor is outside editable sections since it suggests that users can edit non-editable html.
* To avoid this confusion the cursor is hidden when the selection contains non-editable content.
*/
/*
* The selection start/end node/indexes are virtual. Virtual node/indexes are nodes/index in the document
* when there is no highlighed dom. Actual node/indexes are nodes/indexes in the document at the current state
* which is effected by highlighted dom.
*/
var selStartNode = null,
selStartIndex = null,
selEndNode = null,
selEndIndex = null,
highlightFragment = null,
fragmentOpList = null,
formatOpList = null,
hightlightCSS = {
/*high: "#1C1C1C",
low: "#FFFFFF"*/
high: "#3B4B5B",
low: "#DFFFFF"
},
settingCursor = false,
/* Used for raising selection start/end MVC events. */
supressSelectionEvents = false,
/*
* Determines how many pixels away from a cursor's charactor/element
* the mouse pointer should be to re-evaluate a new position.
*/
CURSOR_REEVALAUTE_TOLERANCE = 3,
/* Elements to let clicks fall through */
fallthroughElements = $createLookupMap("button,input,select,textarea"),
/* Inline elements which should not be included/bundled with a word (for word selection) */
wordBreakerInlines = $createLookupMap("br,button,img,iframe,map,object,select,textarea,applet"), /* TODO: REFACTOR- SHARE WITH WHITESPACE INTERNALS */
wordBreakerChars = /^\W$/, // TODO: Multilingual support - not just latin alphabet
/* Elements used for focus/selection stealing */
focusContainer, focusStealerEle,
/* True if the last mouse down was in a protected node. False if not. */
clickedProtectedNode;
$enqueueInit("Selection", function(){
// Make as subject
_model(de.selection);
// Disable selection in IE
if (typeof docBody.onselectstart != "undefined")
docBody.onselectstart = function(){
return de.events.consume(window.event)
};
var target = _engine == _Platform.GECKO ? window : document;
_addHandler(target, "mousedown", onMouseDown);
_addHandler(target, "mouseup", onMouseUp);
_addHandler(target, "mousemove", onMouseMove);
_addHandler(target, "dblclick", onDoubleClick);
// Consume ACCEL+A events to prevent select-all
_addHandler(document, "keydown", function(e) {
if (de.events.Keyboard.isAcceleratorDown(e)) {
if (e.keyCode == 65)
return false; // NB: Doesn't work in presto
}
});
// Whenever the cursor is set outside of this module, keep the selection synchronized.
de.cursor.addObserver({
onCursorChanged: function(cDesc){
if (settingCursor)
return;
if (cDesc) {
// Get the cursors virtual node/index
var vni = getVirtualNodeIndex(cDesc.domNode, getSelectionIndexFromCDesc(cDesc));
// Update the selection: If shift is down then set the new range, otherwise
// set selection as a single point.
if (de.events.current && de.events.current.shiftKey && selStartNode)
de.selection.setSelection(selStartNode, selStartIndex, vni.node, vni.index, false);
else
de.selection.setSelection(vni.node, vni.index, null, null, false);
} else
de.selection.clear();
}
});
// Always ensure that the selection is cleared before an action is executed/redone/undone
function onBeforeAction() {
de.selection.clear();
}
de.UndoMan.addObserver({
onBeforeExec : onBeforeAction,
onBeforeUndo : onBeforeAction,
onBeforeRedo : onBeforeAction
});
// Setup the focus steal element
focusContainer = $createElement("div");
_setClassName(focusContainer, _PROTECTED_CLASS);
focusContainer.innerHTML = '';
_setFullStyle(focusContainer, "position:absolute;width:1px;height:1px;display:none;z-index:-500");
focusStealerEle = focusContainer.firstChild;
docBody.appendChild(focusContainer);
}, "Cursor", "UndoMan");
/**
* @param {Event} e A mouse down dom event
*/
function onMouseDown(e){
clickedProtectedNode = 0;
var targetNode = de.events.getEventTarget(e);
// Test if should let event fall through
var nodeName = _nodeName(targetNode);
if (targetNode && fallthroughElements[nodeName]) {
// Alow selection in text boxes
if (nodeName == "textarea" || (nodeName == "input" && targetNode.type == "text")) {
// Get ride of dedit selection/cursor - switch edit paradigm to native text box
de.selection.clear(); // clear selection
de.cursor.setCursor(null); // clear focus
}
return;
}
if (de.events.Mouse.isLeftDown()) {
// Ignore clicks in protected nodes
if (de.doc.isProtectedNode(targetNode)) {
clickedProtectedNode = 1;
return;
}
// Get the cursor position at the mouse x/y coord
var mousePos = de.events.getXYInWindowFromEvent(e),
targetCursorDesc = de.cursor.getCursorDescAtXY(mousePos.x, mousePos.y, targetNode);
if (!targetCursorDesc) {
// Clear any cursor/selection
de.cursor.setCursor(null); // Triggers MVC event and selection will update
return false;
}
// Is the user ranging a selection via the shift key?
if (e.shiftKey && selStartNode) {
// Try and the cursor at the click position
settingCursor = true; // Prevent updating selection due to cursor MVC events
de.cursor.setCursor(targetCursorDesc);
settingCursor = false;
// Update the selection
var vni = getVirtualNodeIndex(targetCursorDesc.domNode, getSelectionIndexFromCDesc(targetCursorDesc));
setSelection(selStartNode, selStartIndex, vni.node, vni.index, false);
} else { // User is clicking in the document
// Set the cursor at the click position
de.cursor.setCursor(targetCursorDesc); // Triggers MVC event and selection will update
// If the cursor was not supported at the target node, then the selection will have cleared... however
// we want to allow for the user to select outside of editable sections
if (!de.cursor.exists() && !de.doc.isProtectedNode(targetCursorDesc.domNode)) {
var vni = getVirtualNodeIndex(targetCursorDesc.domNode, getSelectionIndexFromCDesc(targetCursorDesc));
setSelection(vni.node, vni.index, null, null, false);
}
}
// Ensure that the document has focus... this will ensure that any input controls within the
// document or in the browser loses focus so user input is forwarded to direct edit
// Get the scrollbar state and set the focus stealer position in the viewport
// to avoid scrolling the document
var scrollPos = _getDocumentScrollPos();
// Position the float (container) at the top left of the viewport,
// but if the scroll bars are at zero, then place the float
// outside of the document... this will completely conceal the float
focusContainer.style.left = (scrollPos.left == 0 ? -50 : scrollPos.left + 10) + "px";
focusContainer.style.top = (scrollPos.top == 0 ? -50 : scrollPos.top + 10) + "px";
focusContainer.style.display = "";
focusStealerEle.focus();
focusStealerEle.select();
focusContainer.style.display = "none";
// Disable native selection
return false;
}
}
/**
* Implements manipulating of selection via dragging the mouse.
* @param {Event} e A mouse move dom event
*/
function onMouseMove(e){
if (de.events.Mouse.isLeftDown() && selStartNode && !clickedProtectedNode) { // Is the user dragging the mouse - and changing the selection?
// Avoid firing many selection event whenever the selection changes while dragging
supressSelectionEvents = true;
var mousePos = de.events.getXYInWindowFromEvent(e),
curCDesc = de.cursor.getCurrentCursorDesc();
// Quick-check to see if the mouse pointer is not far from the current cursor
// to avoid re-evaluting the cursors poistion via the relatively expensive dual binsearch
// at every mouse move event:
if (curCDesc &&
mousePos.x >= (curCDesc.x - CURSOR_REEVALAUTE_TOLERANCE) &&
mousePos.x <= (curCDesc.x + curCDesc.width + CURSOR_REEVALAUTE_TOLERANCE) &&
mousePos.y >= (curCDesc.y - CURSOR_REEVALAUTE_TOLERANCE) &&
mousePos.y <= (curCDesc.y + curCDesc.height + CURSOR_REEVALAUTE_TOLERANCE)) {
var updateSelection = 0;
// See is isRightOf flag needs flipping
if (Math.abs(mousePos.x - curCDesc.x) < Math.abs(mousePos.x - (curCDesc.x + curCDesc.width))) {
// The mouse is closer to the left of charactor/element that the cursor is currently at
if (curCDesc.isRightOf) {
// Need to flip the rightOf flag
curCDesc.isRightOf = false; // flip
settingCursor = true;
de.cursor.setCursor(curCDesc);
settingCursor = false;
updateSelection = 1;
}
} else if (!curCDesc.isRightOf) {
// The mouse is closer to the right of charactor/element that the cursor is currently at..
// but the current cursor is to the left of it.
curCDesc.isRightOf = true; // flip
settingCursor = true;
de.cursor.setCursor(curCDesc);
settingCursor = false;
updateSelection = 1;
}
// Update the selection
if (updateSelection) {
var vni = getVirtualNodeIndex(curCDesc.domNode, getSelectionIndexFromCDesc(curCDesc));
setSelection(selStartNode, selStartIndex, vni.node, vni.index, false);
}
} else { // Re-evaluate the cursor's position via coordinates from the mouse event
curCDesc = de.cursor.getCursorDescAtXY(mousePos.x, mousePos.y, de.events.getEventTarget(e));
if (curCDesc && !de.doc.isProtectedNode(curCDesc.domNode)) {
var vni = getVirtualNodeIndex(curCDesc.domNode, getSelectionIndexFromCDesc(curCDesc));
// Update the selection
setSelection(selStartNode, selStartIndex, vni.node, vni.index);
}
}
}
}
/**
* Raises selection changed events for dragged selection
* @param {Object} e
*/
function onMouseUp(e) {
// Was there any selection due to dragging the moust pointer?
if (selStartNode && supressSelectionEvents)
de.selection.fireEvent("SelectionChanged");
// Restore selection supression flag
supressSelectionEvents = false;
}
// TODO: Triple-click to select full phraise
function onDoubleClick(e) {
// Ignore clicks in protected nodes
if (!de.doc.isProtectedNode(de.events.getEventTarget(e))) {
de.selection.clear();
var mousePos = de.events.getXYInWindowFromEvent(e);
var cDesc = de.cursor.getCursorDescAtXY(mousePos.x, mousePos.y, de.events.getEventTarget(e));
// Double-clicked on anything to select?
if (cDesc) {
// Get the word the user selected on if any
var range = de.selection.getWordRangeAt(cDesc.domNode, cDesc.relIndex);
if (range) { // double clicked on word / space
// Set the new selection to select the word
setSelection(range.startNode, range.startIndex, range.endNode, range.endIndex);
}
}
}
return false; // TODO: DOES Disable selection in safari?
}
// See namespace docs
function getVirtualNodeIndex(node, index) {
if (highlightFragment)
return highlightFragment.getOriginalNodeIndex(node, index);
return {node:node,index:index};
}
// See namespace docs
function getActualNodeIndex(node, index) {
if (highlightFragment)
return highlightFragment.getAdjustedNodeIndex(node, index);
return {node:node,index:index};
}
/**
* To be used when wanting the highlighted DOM cleared.
* ONLY TO BE USED IN BRIEF MOMENTS: Always call the false then truee, never leave
* in uneven state.
*
* @param {Boolean} on True to restore highlighting, false to clear any.
*/
_toggleSectionHighlight = function(on) {
if (on && highlightFragment) {
// Restore highlight css
_redoOperations(formatOpList);
} else if (!on && highlightFragment) {
// Remove highlight formatting
_undoOperations(formatOpList);
}
}
/**
* Visually highlights the current selection. Assumes that there is no selection highlight formatting.
* Updates the cursor.
*/
function highlightSelection(){
debug.assert(formatOpList == null && fragmentOpList == null && highlightFragment == null);
debug.assert(selStartNode != null && selEndNode != null);
// Gets range in left-to-right order
var selRange = de.selection.getRange(true);
debug.assert(!_getOperations());
try {
// Get the cursor cursor state
var curCDesc = de.cursor.getCurrentCursorDesc();
highlightFragment = _buildFragment(_getCommonAncestor(selRange.startNode, selRange.endNode),
selRange.startNode,
selRange.startIndex,
selRange.endNode,
selRange.endIndex);
// Get fragment build operations
fragmentOpList = _getOperations() || [];
// Apply the CSS Highlighting
highlightFragment.visit(function(frag){
if (!frag.isShared) {
var domNode = frag.node, highlightNode = null;
if (domNode.nodeType == Node.ELEMENT_NODE)
highlightNode = domNode;
else if (domNode.nodeType == Node.TEXT_NODE && frag.parent.isShared && _doesTextSupportNonWS(domNode)) {
// If this non shared fragment is a text node who's parent is shared, then
// in order to format this node then spans will be added as its parent
highlightNode = $createElement("span");
highlightNode.className = "dehighlight-node";
// Add the format span and move the text node into it
_execOp(_Operation.INSERT_NODE, highlightNode, domNode.parentNode, _indexInParent(domNode));
_execOp(_Operation.REMOVE_NODE, domNode);
_execOp(_Operation.INSERT_NODE, domNode, highlightNode);
}
if (highlightNode) {
// TODO : Use Classes instead?? although setting actual style will have best precedence
// Get background color for this element
/*var bgColor, bgNode = highlightNode;
do {
bgColor = _getComputedStyle(bgNode, "background-color");
bgNode = bgNode.parentNode;
} while (bgNode && bgColor == "transparent"); // CSS Defaut for background color
if (bgColor && bgColor != "") {
bgColor = _getColorRGB(bgColor);
} else
bgColor = [255, 255, 255];
// Get bg color brightness
var intensity = ((bgColor[0] / 255) + (bgColor[1] / 255) + (bgColor[2] / 255)) / 3;
// Override/set background and foreground color style for this element
_execOp(_Operation.SET_CSS_STYLE, highlightNode, "backgroundColor", intensity >= 0.5 ? hightlightCSS.high : hightlightCSS.low);
_execOp(_Operation.SET_CSS_STYLE, highlightNode, "color", intensity >= 0.5 ? hightlightCSS.low : hightlightCSS.high);
*/
// ABOVE KILLS PERFORMANCE
// if (!_isBlockLevel(highlightNode) || !_isAncestor(highlightNode, selRange.endNode)) {
_execOp(_Operation.SET_CSS_STYLE, highlightNode, "backgroundColor", hightlightCSS.high);
_execOp(_Operation.SET_CSS_STYLE, highlightNode, "color", hightlightCSS.low);
// }
}
}
}); // End visiting all fragments
// Get the formatting operations
formatOpList = _getOperations() || [];
// Update the new cursor pos -If there is a cursor, then its node/index may need updating
if (curCDesc) {
var cursorANI = getActualNodeIndex(curCDesc.domNode, curCDesc.relIndex);
if (cursorANI.node != curCDesc.domNode || cursorANI.index != curCDesc.relIndex) {
curCDesc.domNode = cursorANI.node;
curCDesc.relIndex = cursorANI.index;
settingCursor = true;
de.cursor.setCursor(curCDesc);
settingCursor = false;
}
}
} catch (e) {
settingHighlight = false;
selStartNode = selEndNode = highlightFragment = null;
formatOpList = null;
throw e;
}
}
/**
* Un-highlights any previous highlighting if there is any.
* Updates the cursor.
*/
function unHighlightSelection(){
if (highlightFragment) {
// Keep the cursor updated
var curCDesc = de.cursor.getCurrentCursorDesc();
var cursorVNI = curCDesc ? getVirtualNodeIndex(curCDesc.domNode, curCDesc.relIndex) : null;
_undoOperations(formatOpList);
_undoOperations(fragmentOpList);
formatOpList = fragmentOpList = highlightFragment = null;
// If there was a cursor, then update its node / index due to highlighting
if (cursorVNI && (cursorVNI.node != curCDesc.domNode || cursorVNI.index != curCDesc.relIndex)) {
curCDesc.domNode = cursorVNI.node;
curCDesc.relIndex = cursorVNI.index;
settingCursor = true;
de.cursor.setCursor(curCDesc);
settingCursor = false;
}
}
}
/*
* See namespace docs
*/
function setSelection(startNode, startIndex, endNode, endIndex, updateCursor) {
debug.assert(!startNode || (startNode && typeof(startIndex) == "number"));
debug.assert(!endNode || (endNode && typeof(endIndex) == "number"));
// Should the selection be cleared?
if (!startNode) {
de.selection.clear(updateCursor); // Fires selection changed
return;
}
// If the start and end node/index is the same, then nullify the end point.
if (startNode == endNode && startIndex == endIndex)
endNode = null;
// See if selection needs updating
if (startNode == selStartNode && startIndex == selStartIndex &&
((!endNode && !selEndNode) || (endNode == selEndNode && endIndex == selEndIndex)))
return;
// Clear selection highlight if any
unHighlightSelection();
// Set selection model (all virtual)
selStartNode = startNode;
selStartIndex = startIndex;
selEndNode = endNode;
selEndIndex = endIndex;
// If the selection range has an end point then highlight it,
// and adjust the range to exclude any protected nodes
if (selEndNode) {
var startOccursFirst = doesStartOccurFirst(),
procContainer;
// Adjust selection start/end to exclude protected nodes
while(selStartNode) {
procContainer = de.doc.getProtectedNodeContainer(selStartNode);
if (procContainer) {
selStartNode = (startOccursFirst ? procContainer.nextSibling : procContainer.previousSibling);
if (selStartNode)
selStartIndex = startOccursFirst ? 0 : _nodeLength(selStartNode, 1);
} else break;
}
while(selEndNode) {
procContainer = de.doc.getProtectedNodeContainer(selEndNode);
if (procContainer) {
selEndNode = (startOccursFirst ? procContainer.previousSibling : procContainer.nextSibling);
if (selEndNode)
selEndIndex = startOccursFirst ? _nodeLength(selEndNode, 1) : 0;
} else break;
}
var isValid = selStartNode && selEndNode;
if (isValid) {
isValid = false; // Verify valid range
// Relocate protected nodes if they fall into the range
var relocateProcContainers = [];
_visitAllNodes(_getCommonAncestor(selStartNode, selEndNode), selStartNode, startOccursFirst, function(domNode){
procContainer = de.doc.getProtectedNodeContainer(domNode);
debug.assert(!procContainer || (procContainer && (domNode != selStartNode && domNode != selEndNode)));
if (procContainer) relocateProcContainers.push(procContainer);
// Stop the traversal when reached the selection end node
if (domNode == selEndNode) {
isValid = true; // Flag as valid
return false;
}
});
}
if (!isValid) {
//debug.println("WARNING: Attempt to set selection range within a protected node");
de.selection.clear(updateCursor);
return;
}
// Relocate protected containers so they are outside of the selection
for (var i in relocateProcContainers) {
var pc = relocateProcContainers[i];
if (pc.parentNode) // TODO: AND NOT DOCUMENT_FRAGMENT_NODE ??
pc.parentNode.removeChild(pc);
}
for (var i in relocateProcContainers) {
var pc = relocateProcContainers[i];
if (!pc.parentNode)
docBody.appendChild(pc);
}
// Visually highlight range
highlightSelection();
}
// Should the cursor be adjusted to be placed at the start/end of the new range?
if (updateCursor !== false) {
var newCursor = null;
// Only set cursor if the range is editable
if (de.selection.isRangeEditable()) {
var ani = selEndNode ?
getActualNodeIndex(selEndNode, selEndIndex) :
getActualNodeIndex(selStartNode, selStartIndex);
// Adjust index if at end of text run
var isRightOf = false;
if (ani.node.nodeType == Node.TEXT_NODE && ani.index >= _nodeLength(ani.node)) {
isRightOf = true;
ani.index--;
}
//newCursor = de.cursor.createCursorDesc(ani.node, ani.index, isRightOf);
newCursor = de.cursor.getNearestCursorDesc(ani.node, ani.index, isRightOf, true);
}
// Set the cursor
settingCursor = true;
de.cursor.setCursor(newCursor);
settingCursor = false;
}
// Fire selection ended event
if (!supressSelectionEvents)
de.selection.fireEvent("SelectionChanged");
}
/**
* @param {de.cursor.CursorDescriptor} cDesc A cursor descrptor
* @return {Number} The selection index of the given cursor.
*/
function getSelectionIndexFromCDesc(cDesc){
var index = cDesc.relIndex;
if (cDesc.isRightOf && cDesc.domNode.nodeType == Node.TEXT_NODE)
index ++;
return index;
}
/**
* @return {Boolean} True if the selection start occurs before the selection end.
*/
function doesStartOccurFirst() {
if (!selEndNode)
return true;
if (selStartNode == selEndNode)
return selStartIndex < selEndIndex;
// Convert sel start/end virtual range to actual dom nodes
var actualStart = getActualNodeIndex(selStartNode, selStartIndex).node,
actualEnd = getActualNodeIndex(selEndNode, selEndIndex).node;
var startOccursFirst = false;
_visitAllNodes(docBody, actualStart, true, function(domNode){
startOccursFirst = (domNode == actualEnd);
return !startOccursFirst;
});
return startOccursFirst;
}
/**
* @namespace
* Cross-browser DEdit-specific selection. Implemented as a continuous selection model.
*/
de.selection = {
/**
* Sets a new selection.
*
* @param {Node} startNode The starting dom node of the selection range.
*
* @param {Number} startIndex The inclusive start index in the start node.
* Ranges from 0 to the text length for text nodes.
* Where 0 indicates that the range begins at the first char, and text length
* indicates that the range begins directly after the text node, but not including it.
*
* Ranges from 0 to 1 for elements.
* Where 0 indicates that the range includes the element and its decendants,
* and 1 indicates that the range excludes the element and its decendants.
*
*
* @param {Node} endNode The ending dom node of the selection range.
*
* @param {Number} endIndex The inclusive end index in the end node.
* Ranges from 0 to the text length for text nodes.
* Where 0 indicates that the range ends at the first char, and text length
* indicates that the range ends directly after the text node, but not including it.
*
* Ranges from 0 to 1 for elements.
* Where 0 indicates that the range includes the element and its decendants,
* and 1 indicates that the range excludes the element and its decendants.
*/
setSelection: setSelection,
/**
* Clears any current selection in the document.
* Implementaion Note: If there is highlighting, the the DOM will be manipulated and the cursor will be
* updated.
* @param {Boolean} updateCursor (optional) False to supress updating the cursor. Otherwise will destroy the current cursor.
*/
clear: function(updateCursor){
// Restore any highlighting
unHighlightSelection();
// Nullify range
selStartNode = selEndNode = null;
// Update the cursor
if (updateCursor !== false) {
settingCursor = true;
de.cursor.setCursor(null);
settingCursor = false;
}
if (!supressSelectionEvents)
de.selection.fireEvent("SelectionChanged");
},
/**
* Retreives the selection range.
*
* @param {Boolean} inOrder True to get the range in left-to-right traversal order,
* False to get the range as it is (i.e. the selection end may physically appear before the selection start).
*
* @return {Object} The current selections range in the document. Null if there is none.
* The selection range will have the following members:
*
* startNode - the dom node of the beginning of the selection
*
* startIndex - the index of the beginning of the selection.
*
* endNode - the dom node of the end of the selection.
* May not be present - if not, then the selection does not range for more than one charactor/element
* (i.e. no highlighting present).
*
* endIndex - the index of the end of the selection.
* May not be present - if not, then the selection does not range for more than one charactor/element
* (i.e. no highlighting present).
*
* inOrder - True if the start tuple occurs before the end tuple wrt in-order traversal.
*
*/
getRange: function(inOrder) {
if (!selStartNode) return null;
if (selEndNode) { // Is there selection ranging beyond one charactor/element?
var startOccursFirst = doesStartOccurFirst();
// Determine whether the start point occurs before the end point
var declareStartFirst = !inOrder || startOccursFirst;
range = {
inOrder : inOrder || (startOccursFirst == declareStartFirst),
startNode: declareStartFirst ? selStartNode : selEndNode,
startIndex: declareStartFirst ? selStartIndex : selEndIndex,
endNode: declareStartFirst ? selEndNode : selStartNode,
endIndex: declareStartFirst ? selEndIndex : selStartIndex
};
} else {
range = {
startNode: selStartNode,
startIndex: selStartIndex
};
}
return range;
},
/**
* @return {Boolean} True if there is selection which is all editable. False if there is no selection,
* or the selection contains non-editable content.
*/
isRangeEditable : function() {
if (selStartNode) {
var actualStart = getActualNodeIndex(selStartNode, selStartIndex).node;
var actualEnd = selEndNode ? getActualNodeIndex(selEndNode, selEndIndex).node : actualStart;
var ca = _getCommonAncestor(actualStart, actualEnd, true);
return (actualStart != actualEnd && de.doc.isEditSection(ca)) || de.doc.isNodeEditable(ca);
}
return false;
},
/**
* @return {String} "highlight" if there is a highlighted selection,
* "single" if their is only a cursor present (no highlighted range)
* Null if there is nothing selected.
*/
getState : function() {
if (highlightFragment)
return "range";
if (selStartNode)
return "single";
return null;
},
/**
* Converts the given node index into an actual tuple.
*
* An actual tuple is a node and index which is valid while selectoin highlighting is present.
* (Selection highlighting can create new nodes).
*
* @param {Node} node The node to convert
*
* @param {Number} index The index to convert
*
* @return {Object} An node/index tuple of the actual values.
*/
getActualNodeIndex : getActualNodeIndex,
/**
* Converts the given node index into a virtual tuple.
*
* A virtual tuple is a node and index which is valid when all selection highlighting is removed.
* (Selection highlighting can create new nodes).
*
* @param {Node} node The node to convert
*
* @param {Number} index The index to convert
*
* @return {Object} An node/index tuple of the virtual values.
*
*/
getVirtualNodeIndex : getVirtualNodeIndex,
setHightlightCSS: function(){
// TODO..
},
/**
* Removes the current selection from the document (which is undoable - automatically added to undo manager).
* @return {Boolean} True if there was something selected and therefore removed.
* False if there was nothing to remove.
*
* @throws Error if range is not editable.
*
* @see de.selection.isRangeEditable
*/
remove : function() {
var selRange = this.getRange(true);
if (selRange && selRange.endNode) {
if (!this.isRangeEditable())
_error("Attempt to remove selection which contains uneditable content");
// Execute the removal action
de.UndoMan.execute(
"RemoveDOM",
selRange.startNode,
selRange.startIndex,
selRange.endNode,
selRange.endIndex);
return true;
}
return false;
},
/**
* @return {Node} A copy of the selection in the document if there is anything highlighted.
* Null if there is nothing highlighted.
*/
getHighlightedDOM : function() {
// Is there anything highlighted?
if (highlightFragment) {
// Get rid of highlight CSS
_undoOperations(formatOpList);
var highlightRoot = (function trav(frag){
// Shallow copy the fragment's dom node
var clonedNode = frag.node.cloneNode(false);
// Recurse into fragments children and build up cloned dom tree
for (var i in frag.children) {
var child = trav(frag.children[i]);
clonedNode.appendChild(child);
}
return clonedNode;
})(highlightFragment);
// Restore highlight CSS
_redoOperations(formatOpList);
return highlightRoot;
}
return null;
},
/**
* Selects all content in an editable section.
*
* @param {Node} targetES (Optional) An editable selection to select. If not provided the
* current cursor's editable section owner will be selected.
*/
selectAll: function(targetES) {
// If target editable section not provided then get ES at current cursor position
if (!targetES) {
var cDesc = de.cursor.getCurrentCursorDesc();
if (cDesc)
targetES = de.doc.getEditSectionContainer(cDesc.domNode);
}
// Anything to select?
if (targetES) {
de.selection.setSelection(
targetES.firstChild,
0,
targetES.lastChild,
_nodeLength(targetES.lastChild, 1));
}
},
/**
* Selects the start or end of an editable sectoin
* @param {Element} esEle An editable sectoin
* @param {Boolean} start True to select the beginning of the editable section, false for the end
*/
selectES : function(esEle, start) {
debug.assert(de.doc.isEditSection(esEle));
var deepNode = esEle;
if (start) {
while (deepNode.firstChild) {
deepNode = deepNode.firstChild;
}
} else {
while (deepNode.lastChild) {
deepNode = deepNode.lastChild;
}
}
var cDesc = de.cursor.getNearestCursorDesc(deepNode, start ? 0 : _nodeLength(deepNode, 1), !start, !start);
if (cDesc)
de.cursor.setCursor(cDesc);
},
/**
* @param {Node} node A dome node
* @param {Index} index An index (ranges from 0-text-length-1 for text nodes).
* @return {Object} The range of a word at the given index - null if there is no word.
*/
getWordRangeAt : function(node, index) {
if (node.nodeType == Node.TEXT_NODE) {
var range = {
startNode:node,
startIndex:index,
endNode:node,
endIndex:index
};
var checkESBoundry = de.doc.isNodeEditable(node);
// Expand start backward to first occurance whitespace / block exclusive
_visitAllNodes(docBody, node, false, function(domNode) {
if (domNode != node && _findAncestor(domNode, _getCommonAncestor(domNode, node, true), isWordBreakElement, true))
return false; // Abort when break out of inline group
else if (domNode.nodeType == Node.TEXT_NODE) { // Scan for whitespace - extend range backward
for (var i = domNode == node ? index : _nodeLength(domNode) - 1; i >= 0; i--) {
var c = domNode.nodeValue.charAt(i);
//if (_isAllWhiteSpace(c) || c == _NBSP)
if (wordBreakerChars.test(c))
return false;
range.startNode = domNode;
range.startIndex = i;
}
}
});
// Expand start forward to first occurance whitespace / block exclusive
_visitAllNodes(docBody, node, true, function(domNode) {
if (domNode != node && (isWordBreakElement(domNode) || _findAncestor(node, _getCommonAncestor(domNode, node, true), isWordBreakElement, true)))
return false; // Abort when break out of inline group
if (domNode.nodeType == Node.TEXT_NODE) { // Scan for whitespace - extend range backward
for (var i = domNode == node ? index : 0; i < _nodeLength(domNode); i++) {
var c = domNode.nodeValue.charAt(i);
//if (_isAllWhiteSpace(c) || c == _NBSP)
if (wordBreakerChars.test(c))
return false;
range.endNode = domNode;
range.endIndex = i;
}
}
});
range.endIndex++;
return range;
}
function isWordBreakElement(domNode) {
return _isBlockLevel(domNode) || wordBreakerInlines[_nodeName(domNode)] || (checkESBoundry && de.doc.isEditSection(domNode));
}
},
/**
*
* @param {[String]} formatTypes The format types to poll. Case insensitive. See format dom action for format type list
*
* @return {Object} The edit state
*/
getEditState : function(formatTypes) {
var state = {
formatStates : {},
inlineContainerType : null,
textAlign : null,
blockQuote : false
};
// Is highlight?
if (highlightFragment) {
// Remove highlight formatting
_undoOperations(formatOpList);
// Traverse through nodes ... checking inlines for computed CSS and for block-parent types / links
(function trav(frag){
updateState(frag.node);
})(highlightFragment);
// Restore highlight css
_redoOperations(formatOpList);
}
// Is there any selection - and is it a textnode/inline node?
if (selStartNode && (selStartNode.nodeType == Node.TEXT_NODE || _isInlineLevel(selStartNode)))
updateState(selStartNode.nodeType == Node.TEXT_NODE ? selStartNode.parentNode : selStartNode);
// Return the state
return state;
function isBlockQuote(domNode) {
return _nodeName(domNode) == "blockquote";
}
function updateState(node) {
if (_isInlineLevel(node)) {
var eProps = de.doc.getEditProperties(node) || {};
// TODO: Format filtering here? Or at another place - i.e. via keystrokes wouldnt use this function...
// Could have in both places... could always do it in actions
// Determine format states
for (var i in formatTypes) {
var fType = formatTypes[i].toLowerCase();
// Determine formattings
if (!state.formatStates[fType] || state.formatStates[fType] != "mixed") {
var current = node, wasFound = false;
while (current != docBody && de.doc.isNodeEditable(current)) { // for each editable ancestor up to the document body
// Evaluate specific format state on specific node
var res = _formatEnvironment[fType + "Eval"](current);
if (res) {
if (typeof state.formatStates[fType] == "undefined")
state.formatStates[fType] = res.value; // Set first time
else if (state.formatStates[fType] !== res.value) state.formatStates[fType] = "mixed";
wasFound = true;
break;
}
current = current.parentNode;
} // End loop: scanning ancestors formatting
// Was the current format type found?
if (!wasFound)
state.formatStates[fType] = (state.formatStates[fType] === null || typeof state.formatStates[fType] == "undefined") ? null : "mixed";
}
} // End loop: evaluating format states
}
if (node.nodeType == Node.ELEMENT_NODE) {
// Evaluate text alignment
if (state.textAlign != "mixed") {
var alignment = _getComputedStyle(node, "text-align");
if (!alignment)
alignment = "start";
// Left/Right value based on start/end is conditional - depends on if browser is LTR OR RTL
if (alignment == "start")
alignment = _localeDirection == "rtl" ? "right" : "left";
else if (alignment == "start") alignment = _localeDirection == "rtl" ? "left" : "right";
if (!state.textAlign)
state.textAlign = alignment;
else if (alignment != state.textAlign) state.textAlign = "mixed";
}
// Evaluate inline container type
if (state.inlineContainerType != "mixed") {
var iCon = _isBlockLevel(node) ? node : _findAncestor(node, docBody, _isBlockLevel, true);
if (!de.doc.isNodeEditable(iCon))
iCon = null;
var cType = iCon ? _nodeName(iCon) : "none";
if (!state.inlineContainerType)
state.inlineContainerType = cType;
else if (cType != state.inlineContainerType)
state.inlineContainerType = "mixed";
}
// Evaluate blockquote state
if (!state.blockQuote) {
var bq = isBlockQuote(node) ? node : _findAncestor(node, docBody, isBlockQuote, true);
if (de.doc.isNodeEditable(bq))
state.blockQuote = true;
}
}
}
}
};
})();
//End Selection.js
//Start Spell.js
$enqueueInit("Spell", function() {
// Setup auto-selection of marked errors
_addHandler(_engine == _Platform.GECKO ? window : document, "mouseup", function() {
var sel = de.selection.getRange();
if (sel && !sel.endNode) {
// The user clicked in an editable section... i.e. did not range a selection
var spellNode = de.spell.getMarkedAncestor(sel.startNode);
if (spellNode) {
// The user click inside inline content marked as a spelling error
// so select the whole word
de.selection.setSelection({
startNode:spellNode,
startIndex:0,
endNode:spellNode,
endIndex:1
});
}
}
});
}, "Selection"); // add mouseup event after selection's event registration
(function() {
var
/**
* The classname of the wrappers used for marking spelling mistakes
* @final
* @type String
*/
SPELL_MARK_CLASS_NAME = "sw-spell-error",
SPELL_MARK_CLASS_NAME_RE = /^sw-spell-error$/;
/**
* @namespace
*/
de.spell = {
/**
* @param {[Element]} editableSections (Optional) The editable sections to get words from.
* If not provided then all editable sections will be queried.
*
* @return {[String]} The set of words found in all the given editable sections.
*/
getWords : function(editableSections) {
if (!editableSections)
editableSections = de.doc.getAllEditSections();
var words = [], wmap = {};
for (var i in editableSections) {
visitWords(editableSections[i], function(word) {
if (!wmap[word]) { // Unique word?
words.push(word);
wmap[word] = 1; // Track words to build a set
}
});
}
return words;
},
/**
* Scans a given set of editable sections for spelling errors, for each spelling error found
* it wraps them with a span with the spelling error class.
*
* Calling this may result in a undoable action executed
* via the undo manager (where the users can undo themselves).
*
* @param {[String]|String} errors An array of miss-spelt words - or a string of whitespace seporated words
*
* @param {[Element]} editableSections (Optional) An array of editable sections to mark errors for.
* If not provided then all editable sections will be queried.
*/
markWords : function(errors, editableSections) {
if (!editableSections)
editableSections = de.doc.getAllEditSections();
// Convert error array/string into a lookup map
errors = $createLookupMap(typeof errors == "string" ? errors.replace(/\s/g,',') : errors.join(','));
var foundError = 0;
for (var i in editableSections) {
visitWords(editableSections[i], function(word, startNode, startIndex, endNode, endIndex) {
// Is this word considered an error?
if (errors[word]) {
// Wrap the word and get the fragment of the selected word.
// Group all the actions together as one unit.
var frag = de.UndoMan.execute(
foundError ? de.UndoMan.ExecFlag.GROUP : 0,
'SpellMark',
startNode,
startIndex,
endNode,
endIndex + 1);
foundError = 1;
return frag.children[frag.children.length-1].node; // Return the (exclusive) resume node as the last text node that made up the word
}
});
}
},
/**
* Clears all words marked with spelling errors in a given set of editable sections.
* This operation will add a single undoable action to the undo manager if any
* marked spelling errors are found.
*
* @param {[Element]} editableSections (Optional) An array of editable sections to clear marked errors from.
* If not provided then all editable sections will be queried.
*
* @return {Boolean} Evaluates true if some errors were unmarked (i.e. an undoable action was added
* to the undo managaer).
*/
clearAllMarks : function(editableSections) {
if (!editableSections)
editableSections = de.doc.getAllEditSections();
var errorWrappers = [], i;
for (i in editableSections) {
// Find all spelling error wrappers in each editable section
_visitAllNodes(editableSections[i], editableSections[i], true, function(domNode) {
if (de.spell.isSpellErrorWrapper(domNode))
errorWrappers.push(domNode);
});
}
// Remove all the wrappers
for (i in errorWrappers) {
de.UndoMan.execute(i == '0' ? 0 : de.UndoMan.ExecFlag.GROUP, "SpellUnmark", errorWrappers[i]);
}
// If nothing was found then zero will be returned.
return errorWrappers.length;
},
/**
* Clears a specific marked spelling error.
* This will add an action to the undo manager.
*
* @param {Element} markNode The spelling error wrapper to "ignore" (i.e. remove)
*/
ignoreError : function(markNode, execFlags) {
de.UndoMan.execute(execFlags ? execFlags : 0, 'SpellUnmark', markNode);
},
/**
* Replaces an error with a correction.
* This will add an action to the undo manager.
*
* @param {Element} markNode The spelling error wrapper to "correct" (i.e. replace)
* @param {String} correction The word to replace the spelling error with
*/
correctError : function(markNode, correction) {
de.UndoMan.execute('SpellCorrect', markNode, correction);
},
/**
* @param {Node} node A dom node
* @return {Element} The spelling error wrapper which is either the given dom node or a ancestor of the given dom node.
*/
getMarkedAncestor : function(node) {
return this.isSpellErrorWrapper(node) ? node : _findAncestor(node, this.isSpellErrorWrapper);
},
/**
* @param {Node} node The node to test
* @return {Boolean} True iff the node is a spelling error wrapper element.
*/
isSpellErrorWrapper : function(node) {
return _findClassName(node, SPELL_MARK_CLASS_NAME_RE) ? true : false;
},
/**
* Strips spelling wrapper tags from HTML
*
* @param {String} html HTML Markup.
*
* @return {String} The given HTML with all spelling error wrappers removed.
*/
stripSpellWrapperHTML : function(html) {
var re = /([^<]+)<\/span>/i;
// TODO: Can I just use replace rather than dealing with loops all the time?
// Remove spelling wrappers via RE
while (match = re.exec(html)) {
html = html.substr(0, match.index) + match[1] + html.substr(match.index + match[0].length);
}
return html;
}
};
/**
* Scans for words in an editable section.
*
* @param {Element} editableSection The editable section to scan words for
*
* @param {Function} callback The callback function to send words to. Takes 5 args:
* word (string) - The word that was found
* startNode (Node) - The start text node of the word
* startIndex (Number) - The start index of the word within the start text node
* endNode (Node) - The end text node of the word
* endIndex (Number) - The end index of the word within the end text node
*
* Return the node to exclusively resume the in-order traversal from
*/
function visitWords(editableSection, callback) {
var wordBreakerChars = /^[^\w']$/,
resumeNode = editableSection,
startNode,
startIndex,
endNode,
endIndex,
curWord;
while (resumeNode) {
// Continue visiting nodes within the editable section...
_visitAllNodes(editableSection, resumeNode, true, function(domNode) {
// Skip resume node (exclusive start point)
if (resumeNode == domNode)
resumeNode = 0; // Clear current resume node
else {
// For all text nodes within an element (e.g. not within comments, styles or scripts)...
if (domNode.nodeType == Node.TEXT_NODE && domNode.parentNode.nodeType == Node.ELEMENT_NODE) {
// Only allow words to extend between adjacent text nodes .. otherwise
// wrapping the words will become over complex and bloat the code.
if (endNode && domNode.previousSibling != endNode) {
if (checkWord())
return false; // NB: Will revisit this node since will exclusively resume at the previous sibling
}
var str = domNode.nodeValue, i;
// For each charactor in this text node's string
for (i = 0; i < str.length; i++) {
var c = str.charAt(i);
// Is this charactor a part of a word or a word breaker?
if (wordBreakerChars.test(c)) {
// If so then check for a pending word...
if (checkWord())
return false;
} else { // Found word symbol
// Set start node/index for start of new word
if (!startNode) {
startNode = domNode;
startIndex = i;
curWord = "";
}
// Build word
curWord += c;
// Track end point
endNode = domNode;
endIndex = i;
}
} // End loop: parsing words in text node
}
}
});
} // End loop: visiting nodes within the given editable section
// Check any pending word....
checkWord();
/**
* Checks if there is a pending word, if there is then the callback is invoked.
* @return {Node} The node to exclusively resume from if the callback changed the DOM.
*/
function checkWord() {
// Is there a word pending? Exclude words in a package or within a protected heirarchy
if (startNode
&& endNode
&& !de.doc.isProtectedNode(startNode)
&& !de.doc.isNodePackaged(startNode)) {
// Invoke callback
resumeNode = callback(curWord, startNode, startIndex, endNode, endIndex);
// Reset start/end point.
startNode = endNode = 0;
// Return result
return resumeNode;
}
// Reset start/end point.
startNode = endNode = 0;
}
} // End visitWords function
_registerAction("SpellMark", {
/**
* An undoable action: wraps a adjacent group of text nodes with a spelling error wrapper
*
* @param {Node} startNode
* @param {Number} startIndex
* @param {Node} endNode
* @param {Number} endIndex
*/
exec : function(startNode, startIndex, endNode, endIndex) {
// Build a fragment - isolating the word within the text node
var frag = _buildFragment(null, startNode, startIndex, endNode, endIndex),
wrapper = $createElement("span");
// Wrap the text with a span
_setClassName(wrapper, SPELL_MARK_CLASS_NAME);
_execOp(_Operation.INSERT_NODE, wrapper, frag.node, frag.children[0].pos);
for (var i = 0; i < frag.children.length; i++) {
var migrant = frag.children[i].node;
_execOp(_Operation.REMOVE_NODE, migrant);
_execOp(_Operation.INSERT_NODE, migrant, wrapper);
}
return frag;
}
});
_registerAction("SpellUnmark", {
/**
* Removes a wrapper
*
* @param {Element} markedNode A mark wrapper element to remove...
*/
exec : function(markedNode) {
debug.assert(markedNode && de.spell.isSpellErrorWrapper(markedNode));
// Decide whether to remove the wrapper
/*var shouldRemove = _getClassName(node) != SPELL_MARK_CLASS_NAME;
if (!shouldRemove)
shouldRemove = _doesHaveElementStyle(node);
*/
// TODO: REFACTOR THIS OPERATION... IT IS USED EVERYWHERE
while(markedNode.firstChild) {
var migrant = markedNode.firstChild;
_execOp(_Operation.REMOVE_NODE, migrant);
_execOp(_Operation.INSERT_NODE, migrant, markedNode.parentNode, _indexInParent(markedNode));
}
_execOp(_Operation.REMOVE_NODE, markedNode);
}
});
_registerAction("SpellCorrect", {
/**
* Replaces an error with a correction.
*
* @param {Element} markNode The spelling error wrapper to "correct" (i.e. replace)
* @param {String} correction The word to replace the spelling error with
*/
exec : function(markedNode, correction) {
_execOp(_Operation.INSERT_NODE, document.createTextNode(correction), markedNode.parentNode, _indexInParent(markedNode));
_execOp(_Operation.REMOVE_NODE, markedNode);
}
});
})();
//End Spell.js
//Start Typing.js
de.Typing = {}; // Model
(function(){
$enqueueInit("Typing", function() {
_addHandler(document, "keystroke", onKeyStroke);
_model(de.Typing);
}, "MVC");
function onKeyStroke(e, normalizedKey) {
debug.println("Key-Stroke: '" + normalizedKey + "'");
// Fire typing event
var typingEvent = {cancel:false};
de.Typing.fireEvent("Typing", typingEvent, e, normalizedKey);
// Cancel event if requested
if (typingEvent.cancel)
return;
// Is the CTRL (or Apple-func key for mac) down?
if (de.events.Keyboard.isAcceleratorDown(e)) {
switch (normalizedKey.toLowerCase()) {
case "z":
de.UndoMan.undo();
return false;
case "y":
de.UndoMan.redo();
return false;
}
}
// Don't manipulate the selection if it is not editable
if (!de.selection.isRangeEditable() || !de.cursor.exists())
return;
var targetES = de.doc.getEditSectionContainer(de.cursor.getCurrentCursorDesc().domNode);
// Is the CTRL (or Apple-func key for mac) down?
if (de.events.Keyboard.isAcceleratorDown(e)) {
switch (normalizedKey.toLowerCase()) {
case "b":
toggleFormat("bold");
return false;
case "i":
toggleFormat("italics");
return false;
case "u":
toggleFormat("underline");
return false;
case "a": // Select all
de.selection.selectAll(targetES);
return false;
}
} else if (!e.ctrlKey && !e.metaKey && !e.altKey) {
var keyStr = normalizedKey;
if (keyStr) {
if (keyStr.length > 1) {
switch (keyStr) {
case "Space":
keyStr = " ";
break;
case "Tab": // Indentation / promotion
if (_isBlockLevel(targetES) || targetES == docBody) {
var firstBlock = _findAncestor(de.cursor.getCurrentCursorDesc().domNode, docBody, _isBlockLevel, 1);
if (firstBlock && _nodeName(firstBlock) == "li") {
if (e.shiftKey)
de.UndoMan.execute("DemoteItem");
else de.UndoMan.execute("PromoteItem");
} else de.UndoMan.execute("Indent", !e.shiftKey);
}
return false;
case "Delete":
case "Backspace":
if (de.selection.remove()) {
return false;
} else {
// Get the cursor descriptor
var cursorDesc = de.cursor.getCurrentCursorDesc(),
preDesc,
postDesc;
if (keyStr == "Backspace") {
if (cursorDesc.isRightOf && (cursorDesc.placement == (de.cursor.PlacementFlag.AFTER | de.cursor.PlacementFlag.BEFORE))) {
// Delete before/after nodes completely if cursor is directly after them
preDesc = _clone(cursorDesc);
preDesc.isRightOf = false;
} else {
preDesc = de.cursor.getNextCursorMovement(cursorDesc, true);
}
postDesc = cursorDesc;
} else {
// Delete is just like backspace on the right charactor.
preDesc = cursorDesc;
if (!cursorDesc.isRightOf && (cursorDesc.placement == (de.cursor.PlacementFlag.AFTER | de.cursor.PlacementFlag.BEFORE))) {
// Delete before/after nodes completely if cursor is directly after them
postDesc = _clone(cursorDesc);
postDesc.isRightOf = true;
} else {
postDesc = de.cursor.getNextCursorMovement(cursorDesc, false);
}
}
if (preDesc && postDesc) {
// Check that the range is valid
var isEditable = de.doc.isNodeEditable(postDesc.domNode);
if (isEditable && postDesc.domNode != preDesc.domNode) isEditable &= de.doc.isNodeEditable(preDesc.domNode);
if (isEditable) {
var preIndex;
if (preDesc.domNode.nodeType == Node.TEXT_NODE) {
preIndex = preDesc.relIndex;
if (preDesc.isRightOf) preIndex++;
} else {
if (_nodeName(preDesc.domNode) == "br") {
// If the pre desc is a line break, then count how many other line breaks there are between the pre/post desc
var brCount = 0;
_visitNodes(_getCommonAncestor(preDesc.domNode, postDesc.domNode), preDesc.domNode, true, null, function(domNode){
if (_nodeName(domNode) == "br") brCount++;
return domNode != postDesc.domNode;
});
// If there are more than one line breaks, then exclude the starting line break... since only
// one line break should be removed
preIndex = brCount > 1 ? 1 : 0;
} else
preIndex = preDesc.isRightOf ? 1 : 0;
}
var postIndex;
if (postDesc.domNode.nodeType == Node.TEXT_NODE) {
postIndex = postDesc.relIndex;
if (postDesc.isRightOf) postIndex++;
} else {
// If the pre desc is a line break, then count how many other line breaks there are between the pre/post desc
if (_nodeName(postDesc.domNode) == "br") {
var brCount = 0;
_visitNodes(_getCommonAncestor(preDesc.domNode, postDesc.domNode), preDesc.domNode, true, null, function(domNode){
if (_nodeName(domNode) == "br" && !(domNode == preDesc.domNode && preIndex == 1)) brCount++;
return domNode != postDesc.domNode;
});
// If there are more than one line breaks, then exclude the ending line break... since only
// one line break should be removed
postIndex = brCount > 1 ? 0 : 1;
} else
postIndex = postDesc.isRightOf ? 1 : 0;
}
// Get virtual range since cursor node/index may probably be affected by the selection
var preVNI = de.selection.getVirtualNodeIndex(preDesc.domNode, preIndex),
postVNI = de.selection.getVirtualNodeIndex(postDesc.domNode, postIndex);
// See if can use light-weight remove text action
if (preVNI.node == postVNI.node && preVNI.node.nodeType == Node.TEXT_NODE)
de.UndoMan.execute("RemoveText", preVNI.node, preVNI.index, postVNI.index - preVNI.index);
else {
// Check to see that the range does not remove any editable sections
var ca = _getCommonAncestor(preVNI.node, postVNI.node, true);
if (de.doc.isEditSection(ca) || de.doc.isNodeEditable(ca))
de.UndoMan.execute("RemoveDOM", preVNI.node, preVNI.index, postVNI.node, postVNI.index);
}
}
}
// signal that no further event processing from further up the event stack is needed
return false;
}
case "Enter":
de.selection.remove();
var cursorDesc = de.cursor.getCurrentCursorDesc();
debug.assert(cursorDesc != null);
var index = cursorDesc.relIndex;
if (cursorDesc.domNode.nodeType == Node.TEXT_NODE)
index += (cursorDesc.isRightOf ? 1 : 0);
else index = cursorDesc.isRightOf ? 1 : 0;
if (!de.doc.getEditProperties(targetES).singleLine) {
if (e.shiftKey) {
var lbHTML = " ";
// Line breaks may need place holders too
if (!(cursorDesc.domNode.nodeType == Node.TEXT_NODE &&
index < _nodeLength(cursorDesc.domNode))) lbHTML += _getOuterHTML(de.doc.createMNPlaceholder());
de.UndoMan.execute("InsertHTML", lbHTML, cursorDesc.domNode.parentNode, cursorDesc.domNode, index);
} else if (_isBlockLevel(targetES) || targetES == docBody) {
// Should never allow block container to be created in inline editable sections! (Invaid HTML)
de.UndoMan.execute("SplitContainer", cursorDesc.domNode, index);
}
}
return false;
case "Home":
case "End":
// Set the cursor to either the beggining or end of the current editable section.
var isHome = keyStr == "Home",
boundDesc = de.cursor.getNearestCursorDesc(targetES, isHome ? 0 : 1, !isHome, !isHome);
if (boundDesc) {
de.cursor.setCursor(boundDesc);
de.cursor.scrollToCursor();
return false;
}
default:
return true;
}
}
// The key press is a printable charactor.
// Remove any selection there might be
de.selection.remove();
// Get the cursor descriptor
var cursorDesc = de.cursor.getCurrentCursorDesc();
debug.assert(cursorDesc != null);
// Calculate the cursor index
var index = cursorDesc.relIndex;
if (cursorDesc.domNode.nodeType == Node.TEXT_NODE && cursorDesc.isRightOf)
index++;
else if (cursorDesc.domNode.nodeType == Node.ELEMENT_NODE)
index = cursorDesc.isRightOf ? 1 : 0;
// Perform a text insert action via the undo manager
de.UndoMan.execute("InsertText", cursorDesc.domNode, keyStr, index);
return false;
}
}
}
/**
*
* @param {String} formatType
* @return {Boolean} True if something was formatted, false if not
*/
function toggleFormat(formatType) {
// Perform the action via the undo manager
return de.UndoMan.execute("Format", formatType, !de.selection.getEditState([formatType]).formatStates[formatType]) ? true : false;
}
})();
//End Typing.js
//Start TextAlingAction.js
(function() {
var excludeAlignBlocks = $createLookupMap("dt,dd,caption,colgroup,col,thead,tfoot,tbody,legend,optgroup,option,area,frame");
_registerAction("TextAlign", {
/**
* An undoable alignment action. Sets the text alignment for all block levels in given range.
* Creates new containers on the fly if needed.
*
* @author Brook Novak
*
* @param {String} alignment The CSS text-alignment value. Either left, right, center or justify.
*
* @param {Node} startNode (Optional) The starting dom node of the range to align.
* If not provided then the current selection will be used.
* If provieded must also provide endNode
*
* @param {Node} endNode (Optional) The ending dom node of the range to align. Can be the same as start node
* If not provided then the current selection will be used.
*
*/
exec : function(alignment, startNode, endNode) {
// Auto-set range if not provided.
if (!startNode) {
if (!this.selBefore)
return; // Nothing to select
if (this.selBefore.endNode) {
startNode = this.selBeforeOrdered.startNode;
endNode = this.selBeforeOrdered.endNode;
} else
startNode = endNode = this.selBefore.startNode;
}
debug.assert(endNode, "Supplied start node but not the end node");
var containers;
// First check for special case: If the ranges first block level common ancestor
// is a list item, then set alignment for the list item rather than normalizing within a list item.
var ca = _getCommonAncestor(startNode, endNode);
for (var level = 0; level < 2; level++) {
while (ca != docBody && !_isBlockLevel(ca)) {
ca = ca.parentNode;
}
if (ca == docBody)
break;
if (_nodeName(ca) == "li") {
containers = [ca];
break;
}
ca = ca.parentNode;
}
if (!containers)
// Normalize containers in range and get list of all the containers
containers = _getNormalizedContainerRange(startNode, endNode);
for (var i in containers) {
// NOTES: CSS 2+ spec allows text-align style to be applied to all block level elements.
_visitAllNodes(containers[i], containers[i], true, function(domNode) {
if (_isBlockLevel(domNode) && !excludeAlignBlocks[_nodeName(domNode)]) {
_execOp(_Operation.SET_CSS_STYLE, domNode, "textAlign", alignment);
}
});
}
this.selAfter = this.selBefore;
}
});
})();
//end TextAlingAction.js
//start * file: SplitContainerAction.js
(function() {
var duplicateBlockMap = $createLookupMap("p,pre,h1,h2,h3,h4,h5,h6,li,address");
_registerAction("SplitContainer", {
/**
* An undoable action. Splits a block level element's or body node's contents into two.
* Used for creating new block level elements like paragraphs, headings and list items.
*
* @author Brook Novak
*
* @param {Node} domNode The text or element node to split the (splitable) container owner into two.
*
*
* @param {Number} index The index within/next to the dom node to split from, all the remaining contents INCLUDING the index
* will migrate to the new block-level element.
* For text nodes, this ranges from zero to the length of the text. If at the length of the text, then
* the split will begin after the text node.
* For other nodes, this can be zero (split just before the node) or 1 (split after the node).
*
*/
exec : function(domNode, index) {
debug.assert(index >= 0);
debug.assert(
(domNode.nodeType == Node.TEXT_NODE && index <= domNode.nodeValue.length) ||
(domNode.nodeType == Node.ELEMENT_NODE && index <= 1)
);
// Get the range from the starting point (exclusive) to the end of the splitter
// Get the first ancestor block level element which to split at
var splitter = _findAncestor(domNode.parentNode, docBody, _isBlockLevel, true) || docBody,
endRangeNode, // Will be set to a block / doc body
endNodeIndex;
if (!duplicateBlockMap[_nodeName(splitter)]) {
// In the case where new paragraphs are created at the split point due to the splitter not
// being a container element which should be duplicated, the nodes to migrate may not be all
// nodes to the right of the split point within the splitter... instead it should range from the split point
// to (and excluding) the first occurance of a block level element within the splitter container
// Find the first occuring block level element from the split point onwards.. within the
// splitter container... if any...
_visitAllNodes(splitter, domNode, true, function(node){
if (_isBlockLevel(node)) endRangeNode = node;
return endRangeNode == null;
});
// If contains no block level elements from the split point, then set as end of splitter
if (!endRangeNode || endRangeNode == splitter) {
endRangeNode = splitter;
endNodeIndex = 1;
} else if (endRangeNode == domNode)
// Definitly nothing to migrate
endRangeNode = null;
else
// Make sure the range is exclusive of the block level element
endNodeIndex = 0;
} else {
endRangeNode = splitter;
endNodeIndex = 1;
}
var migrantFragment;
if (endRangeNode) {
// Build a migration fragment
migrantFragment = _buildFragment(
splitter,
domNode,
index,
endRangeNode,
endNodeIndex);
// Check to see if anything will be removed
var isAllShared = true;
migrantFragment.visit(function(f) {
if(!f.isShared) isAllShared = false;
return isAllShared;
});
if (isAllShared)
// If nothing will actually be migrated, then get rid of the fragment
migrantFragment = null;
else migrantFragment.disconnect(); // Remove the second half from the document
}
// Create a new container
var insertAfterNode,
container,
subSplitter;
if (duplicateBlockMap[_nodeName(splitter)]) {
// Special case with list items: If the splitter's first block level ancestor is a list item,
// then insert of splitting within the list item, split outside to a new list item
if (_nodeName(splitter) != "li" && splitter != docBody) {
var superSplitter = _findAncestor(splitter.parentNode, docBody, _isBlockLevel, true);
if (superSplitter && _nodeName(superSplitter) == "li") {
subSplitter = splitter;
splitter = superSplitter;
}
}
insertAfterNode = splitter;
container = (splitter.nodeName.charAt(0).toLowerCase() == "h" && !migrantFragment) ?
$createElement("p") : /* If splitting at the end of a heading, split into new empty paragraph. */
splitter.cloneNode(false);
// Default to paragraph
} else container = $createElement("p");
// Discover where to insert the new container (If haven't already)
if (!insertAfterNode) {
if (migrantFragment) { // Insert the container before the disconnected migrants
var startFrag = migrantFragment.getStartFragment();
// Was the start fragment split into two?
if (migrantFragment.wasStartSplit()) {
// Insert after the first half of the divided text node
insertAfterNode = startFrag.getPreSplitNode();
} else if (startFrag.isShared) { // Still remains in the document?
// Insert after this then..
insertAfterNode = startFrag.node;
} else { // Removed from the document?
// Get the first shared ancestor
var fsa = startFrag;
while (!fsa.isShared) {
fsa = fsa.parent;
}
// Does the first shared ancestor have any child nodes in the document?
if (fsa.node.firstChild) {
// Does the shared ancestor have remvoed child fragments?
if (fsa.children.length > 0) {
// Was the first removed child fragment removed after the first child in its parent?
if (fsa.children[0].pos > 0) {
// If so, insert after fsa's remaining child before the start bounds
insertAfterNode = fsa.node.childNodes[fsa.children[0].pos - 1];
} else {
// If not (first frag was the first child in its dom parent), insert as first child
// in the shared ancestor
_execOp(_Operation.INSERT_NODE, container, fsa.node, 0);
insertAfterNode = null;
}
} else
insertAfterNode = fsa.node.lastChild;
} else if (fsa == migrantFragment) { // Is fsa the splitter?
// If the start bounds where fully removed, insert container as first child of the splitter
_execOp(_Operation.INSERT_NODE, container, splitter, 0);
insertAfterNode = null;
} else
insertAfterNode = fsa.node; // if not insert after the first shared ancestor then
}
} else { // Insert the container before or after the split point
if (index == 0) { // Before split point
_execOp(_Operation.INSERT_NODE, container, domNode.parentNode, _indexInParent(domNode));
insertAfterNode = null;
} else insertAfterNode = domNode; // After split point
}
// Now that the insertion point is discovered from the fragments start bounds, make sure the
// insertAfterNode's parent is block level
if (insertAfterNode && insertAfterNode != docBody && !_isBlockLevel(insertAfterNode)) {
while (insertAfterNode != docBody &&
insertAfterNode.parentNode != docBody &&
!_isBlockLevel(insertAfterNode) &&
!_isBlockLevel(insertAfterNode.parentNode)) {
insertAfterNode = insertAfterNode.parentNode;
}
}
}
// Insert the container into the document (if haven't already done so)
if (insertAfterNode)
_execOp(_Operation.INSERT_NODE, container, insertAfterNode.parentNode, _indexInParent(insertAfterNode) + 1);
// Migrate all of the disconnected nodes in the right-side of the split to the new container
if (migrantFragment) {
for (var i in migrantFragment.children) {
var migs = buildMigrants(migrantFragment.children[i]);
if (migs)
_execOp(_Operation.INSERT_NODE, migs, container); // TODO: Check if migrants can validly migrate to the container?
// _isValidRelationship should actually be ok for Descendants/ancestors... not just immediate parents/children.
}
}
// Add any placeholders in the containers if need be
if (_doesNeedMNPlaceholder(container)) // Important to check container first, it could be a child of the splitter
_execOp(_Operation.INSERT_NODE, de.doc.createMNPlaceholder(), container);
if (_doesNeedMNPlaceholder(splitter))
_execOp(_Operation.INSERT_NODE, de.doc.createMNPlaceholder(), splitter);
if (subSplitter && _doesNeedMNPlaceholder(subSplitter))
_execOp(_Operation.INSERT_NODE, de.doc.createMNPlaceholder(), subSplitter);
// Discover the new cursor position
if (this.flags & de.UndoMan.ExecFlag.UPDATE_SELECTION) {
var cDesc = de.cursor.getNearestCursorDesc(container, 0, false, false);
if (cDesc)
this.selAfter = {startNode : cDesc.domNode, startIndex : cDesc.relIndex + (cDesc.domNode.nodeType == Node.TEXT_NODE && cDesc.isRightOf ? 1 : 0)};
}
/**
* Inner helper function
* @param {de.dom.DOMFragment} frag
*/
function buildMigrants(frag) {
var migrantNode;
if (frag.isShared) {
// Check if all descendant fragments from this shared fragment are all shared
var isAllShared = true;
frag.visit(function(f) {
if(!f.isShared) isAllShared = false;
return isAllShared;
})
// Ignore this subtree if they are all shared
if (isAllShared) return null;
// Clone the shared node.. since it still remains in the document...
migrantNode = frag.node.cloneNode(false);
} else { // disconnected
migrantNode = frag.node;
// Remove from parent and remove its children if it has any
if (migrantNode.parentNode && migrantNode.parentNode.nodeType != Node.DOCUMENT_FRAGMENT_NODE)
_execOp(_Operation.REMOVE_NODE, migrantNode);
while(migrantNode.firstChild) {
_execOp(_Operation.REMOVE_NODE, migrantNode.firstChild);
}
}
// Recurse... build all children and link with the current dom node
for (var i in frag.children) {
var descendants = buildMigrants(frag.children[i]);
if (descendants)
_execOp(_Operation.INSERT_NODE, descendants, migrantNode);
}
return migrantNode;
} // End inner buildMigrants
}
});
})();
//end SplitContainerAction.js
//start RemoveTextAction.js
_registerAction("RemoveText", {
/**
* Removes text within a single text node.
*
* @param {Node} textNode The text node to remove text from
*
* @param {Number} index The index at which to begin removing text from.
* Ranges from 0 - textlength - 1
*
* @param {Number} length The amount of charactors to remove start from the given index. Must be at least 1
*/
exec : function(textNode, index, length) {
debug.assert(index >= 0);
debug.assert(length > 0);
debug.assert(textNode.nodeType == Node.TEXT_NODE);
debug.assert((index + length) <= textNode.nodeValue.length);
// Avoid removing text from placeholders
if (de.doc.isESPlaceHolder(textNode, false) || de.doc.isMNPlaceHolder(textNode, false)) {
// Keep current cursor position
if (this.flags & de.UndoMan.ExecFlag.UPDATE_SELECTION) {
var cDesc = de.cursor.getCurrentCursorDesc();
if (cDesc)
this.selAfter = {startNode : cDesc.domNode, startIndex : cDesc.relIndex + (cDesc.domNode.nodeType == Node.TEXT_NODE && cDesc.isRightOf ? 1 : 0)};
}
return;
}
// Check for surrounding whitespace
var convertLeftCA, convertRightCA;
if (index == 0) {
// Look for preceeding whitespace
_visitAllNodes(docBody, textNode, false, function(domNode) {
if (domNode == textNode) return;
var ca = _getCommonAncestor(textNode, domNode);
if (_findAncestor(domNode, ca, _isBlockLevel, true))
return false;
if (domNode.nodeType == Node.TEXT_NODE && _nodeLength(domNode) > 0) {
if (_isAllWhiteSpace(domNode.nodeValue.charAt(_nodeLength(domNode)-1))) {
convertLeftCA = ca;
return false;
}
}
});
} else {
if(_isAllWhiteSpace(textNode.nodeValue.charAt(index-1)))
convertLeftCA = textNode;
}
if ((index + length) == _nodeLength(textNode)) {
// Look for proceeding whitespace
_visitAllNodes(docBody, textNode, true, function(domNode) {
if (domNode == textNode) return;
var ca = _getCommonAncestor(textNode, domNode);
if (_isBlockLevel(domNode) || _findAncestor(textNode, ca, _isBlockLevel, true))
return false;
if (_nodeLength(domNode, 0) > 0) {
if (_isAllWhiteSpace(domNode.nodeValue.charAt(0))) {
convertRightCA = ca;
return false;
}
}
});
} else {
if (_isAllWhiteSpace(textNode.nodeValue.charAt(index + length)))
convertRightCA = textNode;
}
var convertTarget = (convertLeftCA && convertRightCA && convertLeftCA != convertRightCA) ?
_getCommonAncestor(convertLeftCA, convertRightCA, true) : convertLeftCA || convertRightCA;
if (convertTarget)
_convertWSToNBSP(convertTarget);
// Remove the text
_execOp(_Operation.REMOVE_TEXT, textNode, index, length);
// Normalize converted white space
if (convertTarget)
_normalizeNBSP(convertTarget);
// See if need to add a placeholder
var ph, phParent = de.doc.getEditSectionContainer(textNode);
if (phParent && _doesNeedESPlaceholder(phParent))
ph = de.doc.createESPlaceholder(phParent);
else {
phParent = _findAncestor(textNode, docBody, _isBlockLevel, true) || docBody;
if (_doesNeedMNPlaceholder(phParent))
ph = de.doc.createMNPlaceholder();
}
// Add the needed placeholder
if (ph)
_execOp(_Operation.INSERT_NODE, ph, phParent);
// Update the selection if requested
if (this.flags & de.UndoMan.ExecFlag.UPDATE_SELECTION) {
var cDesc = ph ? de.cursor.createCursorDesc(ph, 0, false) :
de.cursor.getNearestCursorDesc(textNode, index == 0 ? 0 : index-1, index > 0, false);
if (cDesc)
this.selAfter = {startNode : cDesc.domNode, startIndex : cDesc.relIndex + (cDesc.domNode.nodeType == Node.TEXT_NODE && cDesc.isRightOf ? 1 : 0)};
}
// If the text node is left without text then get rid of it
if (_nodeLength(textNode) == 0)
_execOp(_Operation.REMOVE_NODE, textNode);
}
});
// end RemoveTextAction.js
// start RemoveDOMAction.js
_registerAction("RemoveDOM",{
/**
* @class
* An undoable dom action. Removes a given range from the document.
*
* @author Brook Novak
*
* @param {Node} startNode The starting dom node of the fragments range.
*
* @param {Number} startIndex The inclusive start index in the start node.
* Ranges from 0 to the text length for text nodes.
* Where 0 indicates that the range begins at the first char, and text length
* indicates that the range begins directly after the text node, but not including it.
*
* Ranges from 0 to 1 for elements.
* Where 0 indicates that the range includes the element and it's decendants,
* and 1 indicates that the range excludes the element and it's decendants.
*
*
* @param {Node} endNode The ending dom node of the fragments range.
*
* @param {Number} endIndex The inclusive end index in the end node.
* Ranges from 0 to the text length for text nodes.
* Where 0 indicates that the range ends just before the text node, but not including it,
* and text length indicates that the range ends at the last charactor in the text run.
*
* Ranges from 0 to 1 for elements.
* Where 0 indicates that the range excludes the element and it's decendants,
* and 1 indicates that the range includes the element and it's decendants.
*
*/
exec : function(startNode, startIndex, endNode, endIndex) {
var fragmentRoot = _buildFragment(_getCommonAncestor(startNode, endNode, false),
startNode, startIndex, endNode, endIndex);
// Convert all whitespaces to non-breaking spaces
_convertWSToNBSP(fragmentRoot.node);
// Collapse the range
var fMigrantNode = fragmentRoot.collapse();
// Normalize any NBSP entities
_normalizeNBSP(fragmentRoot.node);
if (this.flags & de.UndoMan.ExecFlag.UPDATE_SELECTION) {
var newCursorPos;
// If there was a migrant, get the cursor descriptor from this
if (fMigrantNode)
newCursorPos = de.cursor.getNearestCursorDesc(fMigrantNode, 0, false, true);
else {
// Get the deepest node on the starting bounds that still resides in the document
var fragment = fragmentRoot.getStartFragment();
while (fragment.node != docBody && !_isAncestor(docBody, fragment.node)) {
fragment = fragment.parent;
}
var domNode = fragment.node;
// Check to see if there are any nodes in the start-bounds place
if (fragment.children.length > 0 && domNode.childNodes.length > 0 &&
fragment.children[0].pos <= domNode.childNodes.length) {
if (fragment.children[0].pos == 0) {
newCursorPos = de.cursor.getNearestCursorDesc(domNode.firstChild, 0, false, false);
} else {
var cNode = domNode.childNodes[fragment.children[0].pos - 1];
newCursorPos = de.cursor.getNearestCursorDesc(cNode, _nodeLength(cNode, 2) - 1, true, false);
}
} else newCursorPos = de.cursor.getNearestCursorDesc(fragment.node, _nodeLength(fragment.node, 2) - 1, true, true);
}
if (newCursorPos)
this.selAfter = {
startNode : newCursorPos.domNode,
startIndex : newCursorPos.relIndex + (newCursorPos.domNode.nodeType == Node.TEXT_NODE && newCursorPos.isRightOf ? 1 : 0)
};
}
}
});
_registerAction("RemoveNode",{
/**
* A simple undoable action. Removes a dom node from the document.
* Selection will always be cleared afterwards.
*
* @param {Node} node The node to remove
*
*/
exec: function(node) {
_execOp(_Operation.REMOVE_NODE, node);
}
});
// end RemoveDOMAction.js
//start PromoteItemAction.js
_registerAction("PromoteItem", {
exec : function(startNode, endNode) {
var t = this;
// Auto-set range if not provided.
if (!startNode) {
if (!t.selBefore)
return; // Nothing to promote
if (t.selBefore.endNode) {
startNode = t.selBeforeOrdered.startNode;
endNode = t.selBeforeOrdered.endNode;
} else
startNode = endNode = t.selBefore.startNode;
}
debug.assert(endNode, "Supplied start node but not the end node");
var ca = _findAncestor(_getCommonAncestor(startNode, endNode, 1), docBody, function(testNode) {
var nn = _nodeName(testNode);
return nn == "li" || nn == "ol" || nn == "ul";
}, 1);
// Found a list item / list container in the given range?
if (ca) {
// Get all list items in the given range
var listItems = [];
if (_nodeName(ca) == "li")
listItems.push(ca);
else {
_visitAllNodes(ca, startNode, true, function(domNode){
if (_nodeName(domNode) == "li") listItems.push(domNode);
return domNode != endNode;
});
// The traversal above will skip the start node's li
var li = _findAncestor(startNode, ca, function(testNode) {return _nodeName(testNode) == "li";}, 1);
if (li)
listItems.push(li);
}
// Promote LI: Top => down
for (var i in listItems) {
promoteLI(listItems[i]);
}
}
t.selAfter = t.selBefore;
function promoteLI(li) {
// Can this list item be promoted? It must have a list item at the same level preceeding it
var prevLICon = li.previousSibling, nextLICon = li.nextSibling;
while (prevLICon && prevLICon.nodeType == Node.TEXT_NODE) {
prevLICon = prevLICon.previousSibling;
}
while (nextLICon && nextLICon.nodeType == Node.TEXT_NODE) {
nextLICon = nextLICon.nextSibling;
}
var subList;
if (prevLICon && (_nodeName(prevLICon) == "ol" || _nodeName(prevLICon) == "ul")) // Add to end of exisiting sb-list within previous LI?
subList = prevLICon;
else {
// Create a new LI->[OL/UL] and insert it before the LI to promote
subList = li.parentNode.cloneNode(false);
_execOp(_Operation.INSERT_NODE, subList, li.parentNode, _indexInParent(li));
}
// Move the LI into the sublist
_execOp(_Operation.REMOVE_NODE, li);
_execOp(_Operation.INSERT_NODE, li, subList);
// Check if need to merge follow list items
if (nextLICon && _nodeName(nextLICon) == _nodeName(subList)) {
// If so then merge the list containers
// Merge the lists
_execOp(_Operation.REMOVE_NODE, nextLICon);
while(nextLICon.firstChild) {
var migrant = nextLICon.firstChild;
_execOp(_Operation.REMOVE_NODE, migrant);
_execOp(_Operation.INSERT_NODE, migrant, subList);
}
}
}
}
});
// end PromoteItemAction.js
//start ModifyTableAction.js
(function() {
_registerAction("ModifyTable", {
/**
* An undoable action. Provides a range of table modification operations
*
* @author Brook Novak
*
* @param {String} op The operation name (case insensitive). Can be:
* insert-rows-before-n, Where n is the amount
* insert-rows-after-n, Where n is the amount
* delete-rows,
* insert-cols-before-n, Where n is the amount
* insert-cols-after-n, Where n is the amount
* delete-cols,
* merge-cells,
* split-cells,
* delete-table
*
* @param {Node} startNode The starting dom node of the range to align.
*
* @param {Node} endNode The ending dom node of the range to align. Can be the same as start node
*
* @throws {Error} If given operation is unknown/malformed.
*
*/
exec : function(op, startNode, endNode) {
var tableNode = null,
selectedNodes = [];
// Find first occurance of a table (it does not make sense to modify multiple tables)
_visitAllNodes(_getCommonAncestor(startNode, endNode), startNode, true, function(domNode) {
// Find table
if (!tableNode)
tableNode = _findAncestor(domNode, docBody, function(node){ return (_nodeName(node) == "table"); }, true);
// Record selected nodes
selectedNodes.push(domNode);
return domNode != endNode; // Traverse only in range
});
// Is a table selected?
if (tableNode) {
var tableSelection = {
start : null, /* [row-index,col-index,node] */
end : null /* [row-index,col-index,node] */
};
// Get selection within table
for (var y = 0; y < tableNode.rows.length; y++) { // For each row
var row = tableNode.rows[y];
for (var x = 0; x < row.cells.length; x++) { // For each cell within row
var cell = row.cells[x];
// Determine if this cell is selected
for (var i in selectedNodes) {
if (selectedNodes[i] == cell || _isAncestor(cell, selectedNodes[i])) {
// Update table selection info
if (!tableSelection.start)
tableSelection.start = [parseInt(y),parseInt(x), cell];
tableSelection.end = [parseInt(y),parseInt(x), cell];
break;
}
}
}
}
if (tableSelection.start) { // Is anything in the table selection / does have content to modify?
// Determine table operation
op = op.toLowerCase().split('-');
switch(op[0]) {
case "insert":
if (op.length == 4) {
// Determine amount of rows / cols to insert
var amount = parseInt(op[3]);
if (isNaN(amount))
_error(_ErrorCode.BAD_ARGS);
// Insert the rows/cols
if (op[1] == "rows")
insertRows(tableNode, tableSelection, op[2] == "before", amount);
else if (op[1] == "cols")
insertCols(tableNode, tableSelection, op[2] == "before", amount);
else _error(_ErrorCode.BAD_ARGS);
}
else _error(_ErrorCode.BAD_ARGS);
break;
case "delete":
if (op.length != 2)
_error(_ErrorCode.BAD_ARGS);
switch(op[1]) {
case "table":
deleteTable(tableNode);
break;
case "rows":
deleteRows(tableNode, tableSelection);
break;
case "cols":
deleteCols(tableNode, tableSelection);
break;
default:
_error(_ErrorCode.BAD_ARGS);
}
break;
case "merge":
break;
case "split":
break;
default:
_error(_ErrorCode.BAD_ARGS);
}
// TODO: UPDATE CURSOR
}
}
}
});
/**
* Inserts a new table cell containing a placeholder.
* @param {Node} row The tr element to insert a row into
* @param {Number} index The index of the cell to insert.
*/
function insertCell(row, index) {
// Insert a new cell
var newCell = _execOp(_Operation.INSERT_CELL, row, index);
// Add placeholder markup
de.UndoMan.execute(
de.UndoMan.ExecFlag.DONT_STORE,
"InsertHTML", "
",
newCell);
}
/**
* Inserts one or more rows into a table
* @param {Node} table The table to insert rows into
* @param {Object} selection The table selection
* @param {Boolean} isBefore True to insert before selection, false after
* @param {Number} amount The amount of rows to add
*/
function insertRows(table, selection, isBefore, amount) {
// Determine insertion index
var newRowIndex;
if (isBefore)
newRowIndex = selection.start[0];
else { // insert after - must consider row-span
newRowIndex = selection.end[0] + 1;
var rspan = selection.end[2].getAttribute("rowspan");
if (rspan) newRowIndex += (parseInt(rspan)-1);
}
// Clamp
if (newRowIndex > table.rows.length)
newRowIndex = table.rows.length;
// Determine column count for create cols per new row
var columnCount = getColumnCount(table);
for (var i = 0; i < amount; i++) {
// Create the new row
var newRow = _execOp(_Operation.INSERT_ROW, table, newRowIndex);
// Insert empty cells to fill out row
for (var j = 0; j < columnCount; j++) {
insertCell(newRow, 0);
}
}
}
/**
*
* @param {Object} selection The table selection
* @param {Boolean} isBefore True to get column index before selection, false after
* @return {Number} The column index before/after the given selection
*/
function getColumnIndex(selection, isBefore) {
var index;
if (isBefore) {
index = Math.min(selection.start[1], selection.end[1]);
} else { // insert after - must consider col-span
var startIndex = selection.start[1],
endIndex = selection.end[1];
var cspan = selection.start[2].getAttribute("colspan");
if (cspan) startIndex += (parseInt(cspan)-1);
cspan = selection.end[2].getAttribute("colspan");
if (cspan) endIndex += (parseInt(cspan)-1);
index = Math.max(startIndex, endIndex);
}
return index;
}
/**
* @param {Node} table The table to check
* @return {Number} The amount of columns in the given table
*/
function getColumnCount(table) {
var columnCount = 0;
for (var y = 0; y < table.rows.length; y++) {
columnCount = Math.max(columnCount, table.rows[y].cells.length);
}
return columnCount;
}
/**
* Inserts one or more columns into a table
* @param {Node} table The table to insert columns into
* @param {Object} selection The table selection
* @param {Boolean} isBefore True to insert before selection, false after
* @param {Number} amount The amount of columns to add
*/
function insertCols(table, selection, isBefore, amount) {
// Determine column insertion index
var newColIndex = getColumnIndex(selection, isBefore);
if (!isBefore)
newColIndex++; // Insert AFTER
for (var i = 0; i < amount; i++) {
for (var y = 0; y < table.rows.length; y++) { // For each row in table
var row = table.rows[y];
// Determine insert index ... may not be newColIndex due to colspans
var spannedIndex = -1,
insertIndex = newColIndex;
for (var x = 0; x < row.cells.length; x++) { // For each cell in current row
var cell = row.cells[x];
spannedIndex ++;
// Add column span if has span length more than 1
var cspan = cell.getAttribute("colspan");
if (cspan) {
spannedIndex += (parseInt(cspan) - 1);
}
// Break if found insert index
if (spannedIndex >= newColIndex) {
insertIndex = parseInt(x);
break;
}
} // End loop: each cell
// Insert the new cell which makes up the new column in this row
insertCell(row, insertIndex);
} // End loop: each row
} // End loop: insertion amount
}
/**
* Deletes an entire table
* @param {Node} table The table element to delete
*/
function deleteTable(table) {
_execOp(_Operation.REMOVE_NODE, table);
}
/**
* Deletes all selected rows in table.
* Deletes entire table if all rows selected.
*
* @param {Node} table The table element to delete rows from
* @param {Object} selection The table selection
*/
function deleteRows(table, selection) {
// Is all table selected?
if (selection.start[0] == 0 && selection.end[0] >= (table.rows.length-1))
deleteTable(table);
else {
for (var i = 0; i <= (selection.end[0] - selection.start[0]); i++) {
_execOp(_Operation.DELETE_ROW, table, selection.start[0]);
}
}
}
/**
* Deletes all selected columns in table.
* Deletes entire table if all columns selected.
*
* @param {Node} table The table element to delete columns from
* @param {Object} selection The table selection
*/
function deleteCols(table, selection) {
var colStartIndex = getColumnIndex(selection, true),
colEndIndex = getColumnIndex(selection, false),
columnCount = getColumnCount(table);
// Check if need to delete entire table
if (colStartIndex == 0 && colEndIndex >= (columnCount-1))
deleteTable(table);
else {
for (var i = 0; i <= (colEndIndex - colStartIndex); i++) {
for (var y = 0; y < table.rows.length; y++) { // For each row in table
var row = table.rows[y];
// Determine deletion index ... may not be colStartIndex due to colspans
var spannedIndex = -1,
deleteIndex = colStartIndex;
for (var x = 0; x < row.cells.length; x++) { // For each cell in current row
var cell = row.cells[x];
spannedIndex ++;
// Add column span if has span length more than 1
var cspan = cell.getAttribute("colspan");
if (cspan) {
spannedIndex += (parseInt(cspan) - 1);
}
// Break if found insert index
if (spannedIndex >= colStartIndex) {
deleteIndex = parseInt(x);
break;
}
} // End loop: each cell
// Delete the cell
_execOp(_Operation.DELETE_CELL, row, deleteIndex);
} // End loop: each row
} // End loop: deleting columns
}
}
function mergeCells(table, selection) {
/*
var mergeWidth = (selection.end[1] - selection.start[1]) + 1,
mergeHeight = (selection.end[0] - selection.start[0]) + 1,
mergeTarget = ?;
for (var y = 0; y < mergeHeight; y++) { // Merge all adjacent cells in y-axis (colums)
// Merge this row into merge target
for (var x = 1; x < mergeWidth; x++) { // Merge row into target
_execOp(_Operation.DELETE_CELL, table, selection.start[0]);
x += colspanextra;
}
}*/
}
function splitCells(selectedNodes) {
for (var i in selectedNodes) {
var node = selectedNodes[i];
if (_nodeName(node) == "td" || _nodeName(node) == "th") { // Is cell?
// Does this selected cell have a column span?
var span = node.getAttribute("colspan");
if (span)
_execOp(_Operation.SET_ATTRIBUTE, node, "colspan", ""); // Erase span
var span = node.getAttribute("rowspan");
if (span)
_execOp(_Operation.SET_ATTRIBUTE, node, "rowspan", ""); // Erase span
}
}
}
})();
//End ModifyTableAction.js
//Start ItemizeAction.js
(function() {
/*
* Lookup map containing element names which when itemized should be converted to a list item rather than being
* a descendant of the list items.
*/
var convertContainers = $createLookupMap("p, div");
_registerAction("Itemize", {
/**
* An undoable itemize action. Creates/converts/destroys a list of items in a given range
*
* @author Brook Novak
*
*
* @param {Boolean} bullets True to itemize as bullet list, false to itemize as numbered list.
*
* @param {Node} startNode (Optional) The starting dom node of the range to align.
* If not provided then the current selection will be used.
* If provieded must also provide endNode
*
* @param {Node} endNode (Optional) The ending dom node of the range to align. Can be the same as start node
* If not provided then the current selection will be used.
*
*/
exec : function(bullets, startNode, endNode) {
// Auto-set range if not provided.
if (!startNode) {
if (!this.selBefore)
return; // Nothing to select
if (this.selBefore.endNode) {
startNode = this.selBeforeOrdered.startNode;
endNode = this.selBeforeOrdered.endNode;
} else
startNode = endNode = this.selBefore.startNode;
}
debug.assert(endNode, "Supplied start node but not the end node");
// Perfom execute (recursive operation)
(function exec(start, end, destroy, normalizedRange) {
// Get master container
var masterContainer = _findAncestor(
_getCommonAncestor(start, end, true),
docBody,
_isBlockLevel,
true
) || docBody, destroyListItems, itemizeContainers;
// Deepen start/end range
var deepStart = start, deepEnd = end;
while (deepStart.firstChild) {
deepStart = deepStart.firstChild;
}
while (deepEnd.lastChild) {
deepEnd = deepEnd.lastChild;
}
// Handle special case 1: range within tables
if (_isTableElement(masterContainer) &&
_nodeName(masterContainer) != "td" &&
_nodeName(masterContainer) != "th") {
// Get all the table's cells within the range
var tabCells = [];
_visitAllNodes(masterContainer, masterContainer, true, function(domNode) {
if (domNode.nodeType == Node.ELEMENT_NODE &&
(_nodeName(domNode) == "td" || _nodeName(domNode) == "th")) {
tabCells.push(domNode);
return (domNode != deepEnd && !_isAncestor(domNode, deepEnd));
}
return domNode != deepEnd;
});
// Check if all cells contains nothing but list items and normalize their contents
var areAllListEles = true, normalizedRanges = [];
for (var i in tabCells) {
var res = areAllListElements(tabCells[i], tabCells[i]);
if (res) {
areAllListEles &= res.allListEles;
normalizedRanges.push(res.nrange);
} else normalizedRanges.push(0);
}
// Recurse on table cell
for (var i in tabCells) {
if (normalizedRanges[i])
exec(tabCells[i], tabCells[i], areAllListEles, normalizedRanges[i]);
}
return;
}
// If master container is a list container itself, adjust master container to its parent
if (_nodeName(masterContainer) == "ul" || _nodeName(masterContainer) == "ol")
masterContainer = masterContainer.parentNode;
// Handle special case 2: range within a list item
var ca = masterContainer;
for (var level = 0; level < 2 && ca; level++) {
if (_nodeName(ca) == "li") {
// Determine if need to destroy list item or convert it to a different type
if ((normalizedRange && destroy) ||
(!normalizedRange &&
((bullets && _nodeName(ca.parentNode) == "ul") ||
(!bullets && _nodeName(ca.parentNode) == "ol")))) {
destroyListItems = [ca]; // the list item is the target type
} else {
itemizeContainers = [ca];
masterContainer = ca.parentNode;
}
break;
}
ca = _findAncestor(ca.parentNode, docBody, _isBlockLevel, true);
}
// Check if all containers in range are list-items/list-containers
if (!normalizedRange && !destroyListItems && !itemizeContainers) {
var res = areAllListElements(start, end);
if (res) {
normalizedRange = res.nrange;
// If the range contains all list elements then destroy them
if (res.allListEles)
destroyListItems = normalizedRange;
else
itemizeContainers = normalizedRange;
} else
return;
} else if (normalizedRange) {
// If recursing on a table cell then the destroy/create operation will be predetermined
if (destroy)
destroyListItems = normalizedRange;
else
itemizeContainers = normalizedRange;
}
if (destroyListItems) {
// Tear down all list-items / list-containers.
for (var i in destroyListItems) {
var cont = destroyListItems[i];
var contName = _nodeName(cont);
if (contName == "li") {
convertLI(cont, true);
} else if (contName == "ul" || contName == "ol") {
// Determine range of list items within list container
var li = _isAncestor(cont, start) ? _findAncestor(start, cont) : cont.firstChild,
endLI = _isAncestor(cont, end) ? _findAncestor(end, cont) : cont.lastChild;
while (li) {
var removeLI = li;
li = li == endLI ? null : li.nextSibling;
if (_nodeName(removeLI) == "li") // ensure not white space
convertLI(removeLI, true);
} // End loop: destroying range of list items in list container
}
} // End loop: destroying list items
} else { // Creation of list items
debug.assert(itemizeContainers ? true : false);
// Create the new list container
var listContainer;
// Itemize all containers in range
for (var i in itemizeContainers) {
var cont = itemizeContainers[i];
var cRoot = cont == masterContainer ? cont : _findAncestor(cont, masterContainer),
contName = _nodeName(cont);
if (contName == "li") {
if (i == '0') {
listContainer = convertLI(cont, false);
} else {
var liParent = cont.parentNode;
_execOp(_Operation.REMOVE_NODE, cont);
_execOp(_Operation.INSERT_NODE, cont, listContainer);
// If the list items container is left without any other list items, remove it
var child = liParent.firstChild;
while (child && _nodeName(child) != "li") {
child = child.nextSibling;
}
if (!child)
_execOp(_Operation.REMOVE_NODE, liParent);
}
} else {
if (i == '0') {
listContainer = $createElement(bullets ? "ul" : "ol");
// Determine where to insert the list container
if (contName == "ul" || contName == "ol") {
var shouldInsertBefore = false;
var firstLI = cont.firstChild;
while (firstLI && _nodeName(firstLI) != "li") {
shouldInsertBefore |= (firstLI == start); // possible start node is white space
firstLI = firstLI.nextSibling;
}
if (firstLI)
shouldInsertBefore |= _isAncestor(firstLI, start);
// Insert the list container before this list container if
// the range starts at the first list item.
_execOp(_Operation.INSERT_NODE, listContainer, cRoot.parentNode, _indexInParent(cRoot) + (shouldInsertBefore ? 0 : 1));
} else
_execOp(_Operation.INSERT_NODE, listContainer, cRoot.parentNode, _indexInParent(cRoot) + 1);
}
if (contName == "ul" || contName == "ol") { // List container?
// Migrate list items in the container within the start/end range
var lcChild = _isAncestor(cont, start) ? _findAncestor(start, cont) : cont.firstChild, lcEndChild = _isAncestor(cont, end) ? _findAncestor(end, cont) : cont.lastChild;
while (lcChild) {
var migrateLI = _nodeName(lcChild) == "li" ? lcChild : null;
lcChild = lcChild == lcEndChild ? null : lcChild.nextSibling;
// NB: Keeping white space... since undo history might rely on them
if (migrateLI) {
// Migrate the list item into the new list item container
_execOp(_Operation.REMOVE_NODE, migrateLI);
_execOp(_Operation.INSERT_NODE, migrateLI, listContainer);
// If the list-container is encapsulated by inline elements before the master container,
// then the migrated list items children must also be encapsulated.
// TODO: PHASE THIS OUT - SUPPORTS INVLAID HTML
if (cRoot != cont) {
var liChild = migrateLI.firstChild;
while (liChild) { // For all list items immediate children
if (liChild.nodeType != Node.ELEMENT_NODE && liChild.nodeType != Node.TEXT_NODE) continue;
// Create a cloned sub-tree of the list containers inline parents
var inode = cont.parentNode, iSubTree = [];
while (inode) {
iSubTree.push(inode.cloneNode(false));
inode = inode == cRoot ? null : inode.parentNode;
}
// Connect up unary inline tree
for (var k = iSubTree.length - 1; k > 0; k--) {
iSubTree[k].appendChild(iSubTree[k - 1]);
}
// Insert the cloned inline sub tree in the migrated list item
// such that it encapsulated this list item child
_execOp(_Operation.INSERT_NODE, iSubTree[iSubTree.length - 1], migrateLI, _indexInParent(liChild));
_execOp(_Operation.REMOVE_NODE, liChild);
_execOp(_Operation.INSERT_NODE, liChild, iSubTree[0]);
liChild = iSubTree[iSubTree.length - 1].nextSibling;
} // End loop: encapsulating list item's contents with inline elements
}
}
} // End loop: migrating list items from an existing list container to the new list container
// Check if the container should be removed from the document.. if
// it no longer contains any list items
var shouldRemoveCont = true;
lcChild = cont.firstChild;
while (lcChild) {
if (_nodeName(lcChild) == "li") {
shouldRemoveCont = false;
break;
}
lcChild = lcChild.nextSibling;
}
if (shouldRemoveCont)
_execOp(_Operation.REMOVE_NODE, cRoot); // Remove by the root
} else {
// Create/add a new list item
var listItem = $createElement("li");
_execOp(_Operation.INSERT_NODE, listItem, listContainer);
// Migrate contents
_execOp(_Operation.REMOVE_NODE, cRoot);
_execOp(_Operation.INSERT_NODE, cRoot, listItem);
// Should this container element be converted into a list item? i.e.
// removed from within the list item it was just added to?
if (convertContainers[contName]) {
var parent = cont.parentNode; // Might not be the list container
// Remove container from the document
_execOp(_Operation.REMOVE_NODE, cont);
// Migrate children into the containers old parent
while (cont.firstChild) {
var miNode = cont.firstChild;
_execOp(_Operation.REMOVE_NODE, miNode);
_execOp(_Operation.INSERT_NODE, miNode, parent);
}
}
}
}
} // End loop: itemizing containes in range
}
/**
* Converts a list item, either migrates its contents up one level or changes the list item type from
* numbers to bullets or vice verse.
*
* @param {Node} liEle A list item ("li") element.
* @param {Boolean} destroy True to migrate contents up one level, false to change type.
*
* @return {Node} If destroy is false, it will return the new list container created for changing the list item type.
*/
function convertLI(liEle, destroy) {
var sib = liEle.previousSibling,
doesHavePreceedingLI = false,
doesHaveProceedingLI = false,
lcEle = liEle.parentNode,
didSplit = false,
newLCont = null;
// Determine if the list item is preceeded with other list items
while(!doesHavePreceedingLI && sib) {
doesHavePreceedingLI |= _nodeName(sib) == "li";
sib = sib.previousSibling;
}
// Determine if the list item is proceeded with other list items
sib = liEle.nextSibling
while(!doesHaveProceedingLI && sib) {
doesHaveProceedingLI |= _nodeName(sib) == "li";
sib = sib.nextSibling;
}
if (doesHavePreceedingLI) {
lcEle = splitLIContainer(lcEle, liEle, !destroy, true);
didSplit = true;
}
if (destroy) {
// If the list item's contents are not going to be migrated within a lower level list item,
// then the range needs to be normalized such that inline groups are place in paragraphs.
if (_nodeName(lcEle.parentNode) != "li")
_getNormalizedContainerRange(liEle, liEle);
// Migrate the list item's contents before its list container
while (liEle.firstChild) {
var liCh = liEle.firstChild;
_execOp(_Operation.REMOVE_NODE, liCh);
_execOp(_Operation.INSERT_NODE, liCh, lcEle.parentNode, _indexInParent(lcEle));
}
// Remove the list item from its container
_execOp(_Operation.REMOVE_NODE, liEle);
} else { // change type
if (didSplit) {
if (doesHaveProceedingLI) {
// Get next list item element in split container
var nextLI = liEle.nextSibling;
while (_nodeName(nextLI) != "li") {
nextLI = nextLI.nextSibling;
}
// If previously split the container and migrated some remaining li's .. then split
// again on the new container - into a container of the original type.
splitLIContainer(lcEle, nextLI, true, true);
}
newLCont = lcEle;
} else {
// Insert a new container before and migrate the list item
newLCont = splitLIContainer(lcEle, liEle, true, false);
}
}
// Check if the list item container has any list items left
sib = lcEle.firstChild;
while (sib && _nodeName(sib) != "li") {
sib = sib.nextSibling;
}
// If the li element's container is left without a li, remove it from the document...
if (!sib) {
// Remove all ancestors of the container which do not have multiple children to keep html tidy
var remEle = lcEle;
while (remEle.parentNode.childNodes.length == 1 &&
!de.doc.isEditSection(remEle.parentNode) &&
remEle.parentNode != docBody) {
remEle = remEle.parentNode;
}
_execOp(_Operation.REMOVE_NODE, remEle);
}
return newLCont;
} // End inner function: convertLI
/**
* Splits a list item container in two. Records operation.
* @param {Node} lcEle A ul or ol element
* @param {Node} liEle An li element to split from within lcEle
* @param {Boolean} flipType True to have the split container to have a different type than lcEle
* @param {Boolean} downward True to split container from liEle downward - so new container is after lcEle.
* False to split container from liEle upward - so new container is before lcEle.
*
* @return {Node} The split container.
*/
function splitLIContainer(lcEle, liEle, flipType, downward) {
// Split container in two
var splitC = flipType ? $createElement(_nodeName(lcEle) == "ol" ? "ul" : "ol") : lcEle.cloneNode(false);
// Migrate this and all following li's into the split container
if (downward) {
while (liEle) {
var migrateNode = liEle;
liEle = liEle.nextSibling;
_execOp(_Operation.REMOVE_NODE, migrateNode);
_execOp(_Operation.INSERT_NODE, migrateNode, splitC);
}
_execOp(_Operation.INSERT_NODE, splitC, lcEle.parentNode, _indexInParent(lcEle) + 1);
} else { // Upward
while (liEle) {
var migrateNode = liEle;
liEle = liEle.previousSibling;
_execOp(_Operation.REMOVE_NODE, migrateNode);
_execOp(_Operation.INSERT_NODE, migrateNode, splitC, 0);
}
_execOp(_Operation.INSERT_NODE, splitC, lcEle.parentNode, _indexInParent(lcEle));
}
return splitC;
} // End inner function splitLIContainer
/**
* Normalizes the given range and checks if all containers are list-items/list-containers of a certain type
* (ol or ul)
*
* @param {Node} start The start of the range to normalize. This range will be deepend
* @param {Node} end The end of the range to normalize. This range will be deepend
* @return {Object} Either Null if the range does not have containers. Or an object with
* a memeber called "allListEles" which is true iff all
* containers in the normalized range are list-items/list-containers - and
* another memeber "nrange" which contains the normalized range.
*/
function areAllListElements(start, end) {
// Normalize the range
var normalizedRange = _getNormalizedContainerRange(start, end);
// Anything to itemize?
if (normalizedRange.length == 0)
return null;
// Check if range is all list elements
var isAllListEles = true;
for (var i in normalizedRange) {
var cname = _nodeName(normalizedRange[i]);
// Get list item container name
if (cname == "li")
cname = _nodeName(normalizedRange[i].parentNode);
if ((bullets && cname != "ul") || (!bullets && cname != "ol")) {
isAllListEles = false;
break;
}
}
return {allListEles: isAllListEles, nrange: normalizedRange};
} // End inner areAllListElements
})(startNode, endNode); // End recursive exec function
this.selAfter = this.selBefore;
} // End execute function
});
})();
//End ItemizeAction.js
//Start InsertTextAction.js
_registerAction("InsertText", {
exec : function(domNode, text, index) {
debug.assert(index >= 0);
debug.assert(
(domNode.nodeType == Node.TEXT_NODE && index <= domNode.nodeValue.length) ||
(domNode.nodeType == Node.ELEMENT_NODE && index <= 1)
);
text = _parseHTMLString(_escapeTextToHTML(text)); // Escape into HTML and convert back into text with correct encoding
var phRoot;
// If the target node is a placeholder, then the placeholder should be replaced with the new text
if (de.doc.isESPlaceHolder(domNode, false) || de.doc.isMNPlaceHolder(domNode, false)) {
// Get the placeholder root node
phRoot = domNode;
while (!(de.doc.isESPlaceHolder(phRoot, true) || de.doc.isMNPlaceHolder(phRoot, true))) {
phRoot = phRoot.parentNode;
}
domNode = phRoot;
}
if (domNode.nodeType == Node.TEXT_NODE) {
debug.assert(!phRoot);
// Insert the text
_execOp(_Operation.INSERT_TEXT, domNode, text, index);
// Set the cursor to after the inserted text
if (this.flags & de.UndoMan.ExecFlag.UPDATE_SELECTION)
this.selAfter = {startNode : domNode, startIndex: index + text.length};
} else {
// Create a new text node and add it to the document
var textNode = document.createTextNode(text);
_execOp(_Operation.INSERT_NODE, textNode, domNode.parentNode, _indexInParent(domNode) + (index == 0 ? 0 : 1));
// Remove the placeholder from the document if there was any
if (phRoot)
_execOp(_Operation.REMOVE_NODE, phRoot);
if (this.flags & de.UndoMan.ExecFlag.UPDATE_SELECTION)
this.selAfter = {startNode : textNode, startIndex: index + _nodeLength(textNode)};
}
// Normalize any inserted non breaking spaces
if (text.indexOf(_NBSP) != -1)
_normalizeNBSP(domNode.parentNode);
}
});
//End InsertTextAction.js
//start InsertHTMLAction.js
_registerAction("InsertHTML", {
/**
* @class
* An undoable insertion action. Inserts HTML into the document
*
* @author Brook Novak
*
* @param {String} html The HTML to insert.
*
* @param {Node} parentNode The parent node of the HTML to insert into.
*
* @param {Node} refNode The node to insert next to, or inside of (if a text node).
* Null if parentNode has no children, in which case the HTML will be inserted
* as the only children of parentNode.
*
* @param {Number} index If refNode is null this is not applicable.
* If refNode is a Element node, then index can either be 0 or 1. 0 indicates that the
* HTML should be inserted before the refNode, and 1 indicates that it should be inserted after.
* Otherwise, if refNode is a Text Node, then index can range from 0 to the length of the text.
* Where the index indicates that the html should be inserted before the charactor at the given index.
* If index is zero, then the html will be inserted before the text node. If index is the length of the
* text, then the html will be inserted after the text node. Otherwise the HTML text will be
* inserted within the text run... splitting the text node in two.
*
*/
exec : function(html, parentNode, refNode, index) {
debug.assert(refNode || (!refNode && parentNode.childNodes.length == 0));
debug.assert(!refNode || index >= 0);
debug.assert(!refNode ||
(refNode.nodeType == Node.TEXT_NODE && index <= refNode.nodeValue.length) ||
(refNode.nodeType != Node.TEXT_NODE && index <= 1)
);
var domRoots = [];
// Get all root elements in HTML
var domTree = parentNode == docBody ? $createElement("div") : parentNode.cloneNode(false);
domTree.innerHTML = html;
var root = domTree.firstChild;
while(root) {
domRoots.push(root);
root = root.nextSibling;
}
// Disconnect roots from temporary container
for (var i = 0; i < domRoots.length; i++) {
domTree.removeChild(domRoots[i]);
var spanWrapper = 0;
if (domRoots[i].nodeType == Node.TEXT_NODE) {
spanWrapper = $createElement("span");
spanWrapper.appendChild(domRoots[i]);
}
// Add a renderable char at the end of the container... this will avoid
// consildation from changing existing contents
var endNode = document.createTextNode("X");
parentNode.appendChild(endNode);
// Consolidate the dom root's whitespace sequences
parentNode.appendChild(spanWrapper ? spanWrapper : domRoots[i]);
_recordOperations = false;
_consolidateWSSeqs(domRoots[i], false);
_recordOperations = true;
if (spanWrapper) {
domRoots.splice(i, 1); // remove this dom root, it is to be replaced or entirly removes
// Consolidation can completly remove nodes, so test each consolidated sub-tree to
// see if they should be inserted
if (!spanWrapper.firstChild)
i --;
else {
// A text node can be split into several text nodes after consolidation
var wrappedChild;
while(wrappedChild = spanWrapper.firstChild) {
spanWrapper.removeChild(wrappedChild);
domRoots.splice(i, 0, wrappedChild);
i++;
}
i--;
}
parentNode.removeChild(spanWrapper);
} else
parentNode.removeChild(domRoots[i]);
parentNode.removeChild(endNode);
}
// If the target node is a placeholder, then the placeholder should be replaced with the new html
var phRoot;
if (de.doc.isESPlaceHolder(parentNode, false) || de.doc.isMNPlaceHolder(parentNode, false))
phRoot = parentNode;
else if (refNode && (de.doc.isESPlaceHolder(refNode, false) || de.doc.isMNPlaceHolder(refNode, false)))
phRoot = refNode;
if (phRoot) { // Get the placeholder root and adjust the parent node / ref node
while (phRoot.parentNode &&
(de.doc.isESPlaceHolder(phRoot.parentNode, false) || de.doc.isMNPlaceHolder(phRoot.parentNode, false))) {
phRoot = phRoot.parentNode;
}
targetNode = phRoot;
parentNode = phRoot.parentNode;
refNode = phRoot; // See later .. it will be removed
}
var normalizeTargetWS;
// Does the insertion point need to split a text node in two?
if (refNode &&
refNode.nodeType == Node.TEXT_NODE &&
index > 0 &&
index < _nodeLength(refNode)) {
var rem = _execOp(_Operation.SPLIT_TEXT_NODE, refNode, index);
// If inserting HTML an a whitespace point, then must convert to all NBSP then normalize later
if (_isAllWhiteSpace(rem.nodeValue.charAt(0)) || _isAllWhiteSpace(refNode.nodeValue.charAt(index-1))) {
normalizeTargetWS = refNode.parentNode;
_convertWSToNBSP(normalizeTargetWS);
}
}
// Get the sibling node to begin inserting the dom afterwards.
var afterNode;
if (!refNode) afterNode = null;
else if (index == 0) afterNode = refNode.previousSibling;
else afterNode = refNode;
// Insert the DOM nodes
for (var i in domRoots) {
if (!afterNode)
_execOp(_Operation.INSERT_NODE, domRoots[i], parentNode, parentNode.firstChild ? _indexInParent(parentNode.firstChild) : null)
else _execOp(_Operation.INSERT_NODE, domRoots[i], parentNode, _indexInParent(afterNode) + 1);
afterNode = domRoots[i];
}
// Normalize whitespace sequences if need to
if (normalizeTargetWS)
_consolidateWSSeqs(normalizeTargetWS);
// Need to remove any place holders?
if (refNode && (de.doc.isMNPlaceHolder(refNode) || de.doc.isESPlaceHolder(refNode)))
_execOp(_Operation.REMOVE_NODE, refNode);
if (this.flags & de.UndoMan.ExecFlag.UPDATE_SELECTION) {
var cDesc = de.cursor.getNearestCursorDesc(
domRoots[domRoots.length-1],
_nodeLength(domRoots[domRoots.length-1], 2) - 1,
true,
_nodeName(domRoots[domRoots.length-1]) != "br");
if (cDesc)
this.selAfter = {startNode : cDesc.domNode, startIndex : cDesc.relIndex + (cDesc.domNode.nodeType == Node.TEXT_NODE && cDesc.isRightOf ? 1 : 0)};
}
}
});
//End InsertHTMLAction.js
// Start IdenAction.js
(function() {
/**
* The amount of pixels to adjust margins when increasing and decreasing indents.
* @type Number
*/
var INDENT_WIDTH = 20,
undentableBlocks = $createLookupMap("dt,dd,caption,colgroup,col,thead,tfoot,tbody,tr,td,th,legend,optgroup,option,area,frame");
_registerAction("Indent", {
/**
* An undoable indentation action. Increases or decreases left indents on container elements in a given range.
* If nodes in the given range should be indented but do not have a container, a new container is created for them.
*
* @author Brook Novak
*
* @param {Boolean} increase True to increase indents in range, false to decrease indents.
*
* @param {Node} startNode (Optional) The starting dom node of the range to align.
* If not provided then the current selection will be used.
* If provieded must also provide endNode
*
* @param {Node} endNode (Optional) The ending dom node of the range to align. Can be the same as start node
* If not provided then the current selection will be used.
*
*
*/
exec : function(increase, startNode, endNode) {
// Auto-set range if not provided.
if (!startNode) {
if (!this.selBefore)
return; // Nothing to select
if (this.selBefore.endNode) {
startNode = this.selBeforeOrdered.startNode;
endNode = this.selBeforeOrdered.endNode;
} else
startNode = endNode = this.selBefore.startNode;
}
debug.assert(endNode, "Supplied start node but not the end node");
var containers;
// First check for special case: If the ranges first block level common ancestor
// is a list item, then indent the list item rather than normalizing within a list item.
var ca = _getCommonAncestor(startNode, endNode);
for (var level = 0; level < 2; level++) {
while (ca != docBody && !_isBlockLevel(ca)) {
ca = ca.parentNode;
}
if (ca == docBody)
break;
if (_nodeName(ca) == "li") {
containers = [ca];
break;
}
ca = ca.parentNode;
}
if (!containers)
// Normalize containers in range and get list of all the containers
containers = _getNormalizedContainerRange(startNode, endNode);
// Record all indentable containers in the given range
for (var i in containers) {
if (!undentableBlocks[_nodeName(containers[i])]) {
var con = containers[i];
// Determine the containers left margin in pixels
var marginToParse = (con.style.marginLeft && con.style.marginLeft.toLowerCase() != "auto") ?
con.style.marginLeft : null;
// If margin is default/auto, get the computed style to make sure
if (!marginToParse)
marginToParse = _getComputedStyle(con, "margin-left");
marginToParse = (marginToParse && marginToParse.toLowerCase() != "auto") ?
marginToParse : "0";
// Parse intentation numberic value
var marginPixels;
if (marginToParse.indexOf("%") != -1) {
// Convert percentage into pixels by using the containers relative offset
marginPixels = con.offsetLeft;
} else marginPixels = parseInt(marginToParse);
var newMarginLen = marginPixels + ((increase ? 1 : -1) * INDENT_WIDTH);
// Clip margin to zero
if (newMarginLen < 0)
newMarginLen = 0;
// Assign new margin
if (marginPixels != newMarginLen)
_execOp(_Operation.SET_CSS_STYLE, con, "marginLeft", newMarginLen + "px");
}
}
this.selAfter = this.selBefore;
}
});
})();
//End IdentAction.js
//start FormatAction.js
_registerAction("Format", {
/**
* An undoable formatting action. Formats a range of DOM nodes in the document by
* adjusting their CSS styles.
*
* @param {String} type The type of formatting you want to apply. Case insensitive.
* See TODO for a list of standard formatting actions available.
*
* @param {Object} value The value to set - dependant on format type. null or empty to clear formatting.
*
* @param {Object} range (Optional) The DOM Range to apply the formatting to. If omitted then the current
* selection will be used. Must have members:
*
* - startNode The starting dom node of the fragments range.
*
* - startIndex The inclusive start index in the start node.
* Ranges from 0 to the text length for text nodes.
* Where 0 indicates that the range begins at the first char, and text length
* indicates that the range begins directly after the text node, but not including it.
*
* Ranges from 0 to 1 for elements.
* Where 0 indicates that the range includes the element and it's decendants,
* and 1 indicates that the range excludes the element and it's decendants.
*
*
* - (optional) endNode The ending dom node of the fragments range. Omit to select word at given start node/index.
*
* - (optional) endIndex The inclusive end index in the end node. Omit to select word at given start node/index.
* Ranges from 0 to the text length for text nodes.
* Where 0 indicates that the range ends just before the text node, but not including it,
* and text length indicates that the range ends at the last charactor in the text run.
*
* Ranges from 0 to 1 for elements.
* Where 0 indicates that tmFragment(frag) {
* and 1 indicates that the range includes the element and it's decendants.
*
* @return {_Fragment} The fragment which represents the range in the DOM which was formatted. Undefined if nothing formatted.
*/
exec:function(type, value, range) {
type = type.toLowerCase();
// Check if the format type exists
if (typeof _formatEnvironment[type + "Wrapper"] == "undefined") {
debug.assert(false, 'Unknown format request: "' + type + '"');
return;
}
var clear = value ? false : true, isWordRange = false, orderSelAfter = true, formatRange;
// Auto-set range to current selectoin if none is provided
if (!range) {
if (!this.selBefore)
return; // Nothing to format
range = _clone(this.selBeforeOrdered);
formatRange = _clone(range);
// Because the range is based on the current selection, set the selection
// after to maintain the selection before ordering (i.e. keep the cursor
// at the same end of the selection).
orderSelAfter = this.selBefore.inOrder;
} else
formatRange = _clone(range);
// Auto-select word-range - if the range is a single node/index tuple
if (!formatRange.endNode) {
var anchorRange = null;
// Special case: instead of selecting word for links, select the whole anchor
if (type == "link") {
var current = formatRange.startNode;
while (current && de.doc.isNodeEditable(current)) {
if (current.nodeType == Node.ELEMENT_NODE && _nodeName(current) == "a") {
anchorRange = {
startNode : current,
startIndex : 0,
endNode : current,
endIndex : 1 /* Since a element, 1 indicates complete right-most deepest descandant */
};
break;
}
current = current.parentNode;
}
}
formatRange = anchorRange ?
anchorRange
: de.selection.getWordRangeAt(formatRange.startNode, formatRange.startIndex >= _nodeLength(formatRange.startNode) ? _nodeLength(formatRange.startNode) - 1 : formatRange.startIndex);
if (!formatRange)
return; // Nothing to format
isWordRange = orderSelAfter = true;
}
// Execute the action
var fragmentRoot = formatDOMExec(
formatRange.startNode,
formatRange.startIndex,
formatRange.endNode,
formatRange.endIndex,
_formatEnvironment[type + "Wrapper"](value || ""),
clear,
_formatEnvironment[type + "Eval"]);
// Update selection if requested
if (this.flags & de.UndoMan.ExecFlag.UPDATE_SELECTION) {
if (isWordRange) {
// Must get adjusted node/index since fragment will most likely of split some text nodes
var adjustedRange = fragmentRoot.getAdjustedNodeIndex(range.startNode, range.startIndex);
// Set the selection to be the single point
this.selAfter = {
startNode: adjustedRange.node,
startIndex: adjustedRange.index
};
} else {
var startFrag = fragmentRoot.getStartFragment(), endFrag = fragmentRoot.getEndFragment();
var startIndex = 0,
endIndex = _nodeLength(endFrag.node, 1);
this.selAfter = {
startNode: orderSelAfter ? startFrag.node : endFrag.node,
startIndex: orderSelAfter ? startIndex : endIndex,
endNode: orderSelAfter ? endFrag.node : startFrag.node,
endIndex: orderSelAfter ? endIndex : startIndex
};
}
}
/**
* The workhorse for the format action.
*/
function formatDOMExec(startNode, startIndex, endNode, endIndex, inlineWrapper, clear, evalElement) {
var isRecursing = arguments.length > 7,
startExclusive = startIndex == _nodeLength(startNode, 1),
endExclusive = endIndex == 0;
// Build up fragment - split any text nodes where neccessary
var fragmentRoot = _buildFragment(_getCommonAncestor(startNode, endNode, false),
startNode,
startIndex,
endNode,
endIndex);
var startFrag = fragmentRoot.getStartFragment(),
endFrag = fragmentRoot.getEndFragment(),
rootNode = fragmentRoot.node; // Keep track of the root node... it may change if it is removed
// Mark fragments which should not be included for formatting
if (startExclusive)
markFrags(startFrag);
if (endExclusive)
markFrags(endFrag);
function markFrags(markFrag) {
do {
markFrag.dontFormat = 1;
markFrag = markFrag.parent;
} while(markFrag && markFrag.children.length == 1);
}
// PASS 1: Clear all related formatting
// Clear all related formatting within fragment
fragmentRoot.visit(function(frag){
if (!frag.dontFormat)
removeFormatting(frag.node);
});
// Clear all related formatting up to the document root (include body since can have styles/classes)
if (!isRecursing) {
var domNode = rootNode; // NB: Include root in removal since it may no longer be the fragment root
while (domNode) {
removeFormatting(domNode);
if (domNode == docBody) break;
domNode = domNode.parentNode;
} // End clear formatting to to root element
}
if (!clear) { // Should new formatting be applied? I.E. Not just clearing formatting
// PASS 2: Set new formatting
// Rebuild fragment since structure may have changed (this won't create any extra operations)
startNode = startFrag.node;
endNode = endFrag.node;
var newFragmentRoot = _buildFragment(
rootNode,
startNode,
startExclusive ? _nodeLength(startNode, 1) : 0,
endNode,
endExclusive ? 0 : _nodeLength(endNode, 1)
);
if (startExclusive)
markFrags(newFragmentRoot.getStartFragment());
if (endExclusive)
markFrags(newFragmentRoot.getEndFragment());
// NB: Keeping old fragment root in order to determine new cursor position (see below)
// Traverse the fragment structure and encapsulate inline groups where needed
(function trav(frag){
var igroups = [];
// For all sub tress
for (var i in frag.children) {
var child = frag.children[i];
if (shouldEncap(child))
igroups.push(child.node);
else {
// If cannot wrap the subtree due to containing at least one block level element, then
// recurse deeper
encapsulateIGroup(); // wrap anything pending
trav(child);
}
}
// Encap remaining run if any
encapsulateIGroup();
/**
* Checks the local igroups array and moves any nodes in the array into a wrapper
* then clears the array.
*/
function encapsulateIGroup(){
if (igroups.length > 0) {
// Create inline wrapper and inset at beginning of inline group run
var wrapper = inlineWrapper.cloneNode(false);
_execOp(_Operation.INSERT_NODE, wrapper, frag.node, _indexInParent(igroups[0]));
// Move run of inline groups into the wrapper
for (var i in igroups) {
var toEncap = igroups[i];
_execOp(_Operation.REMOVE_NODE, toEncap);
_execOp(_Operation.INSERT_NODE, toEncap, wrapper);
}
// Reset igroup run
igroups = [];
}
} // End encapsulateIGroup function
/**
* @param {Node} node The node to test
* @return {Boolean} True iff the fragments node should be encapsulated.
*/
function shouldEncap(frag) {
// Should this fragment be excluded from formatting?
if (frag.dontFormat)
return false;
// Check that all node inclusive don't contain block-levels
var allin = true;
_visitAllNodes(frag.node, frag.node, true, function(domNode) {
allin = !_isBlockLevel(domNode);
return allin;
})
if (allin) {
// Last check: the fragment mimics the actual document structure.. i.e. not a partial range.
// This avoids encapsulating nodes which are not in the format range
var isStructureSame = true;
(function checkStructure(curFrag, curNode) {
// Avoid including fragments which aren't actually in the format range
if (curFrag.dontFormat) {
isStructureSame = false;
return;
}
if (curNode.firstChild) {
isStructureSame = curNode.childNodes.length == curFrag.children.length;
if (isStructureSame) {
for (var j = 0; j < curNode.childNodes.length; j++) {
checkStructure(curFrag.children[j], curNode.childNodes[j]);
if (!isStructureSame)
break;
}
}
} else {
isStructureSame = curFrag.children.length == 0;
}
})(frag, frag.node);
return isStructureSame;
}
return false;
} // End isAllInline function
})(newFragmentRoot); // End encapsulating inline groups
} // End if: not clearing formatting
return fragmentRoot;
/**
* Removes immediate formatting for the given node.
* May result in removal of node... the local range may be adjusted when removals occur
*
* @param {Node} node a node to remove formatting from
*/
function removeFormatting(node) {
if (node.nodeType == Node.ELEMENT_NODE) {
var shouldRemove = false,
res = evalElement(node); // Determine the actions to take to strip formatting at this element
if (res) { // Does this element contain related formatting?
for (var i in res.strip) {
// Strip formatting
switch (res.strip[i].type) {
case 1: // Name match
shouldRemove = true;
break;
case 2: // Class match
// Remove class... but keep extra classes
var cls = _getClassName(node);
if (cls) { // Strip matched class
var clss = cls.split(' ');
for (var j in clss) {
if (clss[j] == res.strip[i].match) {
clss.splice(j, 1);
break;
}
}
cls = cls.length > 0 ? clss.join(' ') : "";
_execOp(_Operation.SET_CLASS, node, cls);
}
break;
case 3: // Style match
// Erase the matched style
_execOp(_Operation.SET_CSS_STYLE, node, res.strip[i].match, "");
break;
// @DEBUG ON
default:
debug.assert(false, 'Unknown result type "' + res.strip[i].type + '" for stripping formatting');
// @DEBUG OFF
}
// If going to remove then no need to strip anything.
if (shouldRemove) break;
} // End loop: stripping formatting
// Get left-most child of the current sub tree
var leftMost = node.firstChild;
while (leftMost.firstChild)
leftMost = leftMost.firstChild;
// Get right-most child of the current sub tree
var rightMost = node.lastChild;
while (rightMost.lastChild)
rightMost = rightMost.lastChild;
// Check if the node should be auto-removed - only if the element is a generic span and has no class/styles.
if (!shouldRemove && _nodeName(node) == "span") {
shouldRemove = !_getClassName(node);
if (!shouldRemove)
shouldRemove = _doesHaveElementStyle(node);
}
if (shouldRemove) {
// Remove all children and connect with parent
var migrations = [];
while (node.firstChild) {
var migrant = node.firstChild;
_execOp(_Operation.REMOVE_NODE, migrant);
_execOp(_Operation.INSERT_NODE,
migrant,
node.parentNode,
_indexInParent(migrations.length > 0 ? migrations[migrations.length - 1] : node) + 1
);
migrations.push(migrant);
}
// If has no children, then ignore ... code wll bloat and it's too complex to bother tidying such cases
if (migrations.length> 0) {
// If node is the current root node then the root must be adjusted
if (node == rootNode)
rootNode = node.parentNode;
// Remove the actual node
_execOp(_Operation.REMOVE_NODE, node);
}
}
// Ensure that all of the elements descendants that it not within the formatting range
// still inherits the formatting that was just stripped using recursion
if (!isRecursing) {
// Recurse on left-range?
var isInFormatRange = false;
fragmentRoot.visit(function(f){
isInFormatRange = (!f.dontFormat && f.node == leftMost);
return !isInFormatRange;
});
if (!isInFormatRange) { // Do recursion
formatDOMExec(
leftMost,
0,
startFrag.node,
startExclusive ? _nodeLength(startFrag.node, 1) : 0,
res.inline,
false,
evalElement,
true);
}
// Recurse on right-range?
isInFormatRange = false;
fragmentRoot.visit(function(f){
isInFormatRange = (!f.dontFormat && f.node == rightMost);
return !isInFormatRange;
});
if (!isInFormatRange) { // Do recursion
formatDOMExec(
endFrag.node,
endExclusive ? 0 : _nodeLength(endFrag.node, 1),
rightMost,
_nodeLength(rightMost, 1),
res.inline,
false,
evalElement,
true);
}
}
}
}
} // End inner function removeFormatting
} // End inner function formatDOMExec
} // End exec
});
//End FormatAction.js
//Start DemoteItemAction.js
_registerAction("DemoteItem", {
exec : function(startNode, endNode) {
var t = this;
// Auto-set range if not provided.
if (!startNode) {
if (!t.selBefore)
return; // Nothing to promote
if (t.selBefore.endNode) {
startNode = t.selBeforeOrdered.startNode;
endNode = t.selBeforeOrdered.endNode;
} else
startNode = endNode = t.selBefore.startNode;
}
debug.assert(endNode, "Supplied start node but not the end node");
var ca = _findAncestor(_getCommonAncestor(startNode, endNode, 1), docBody, function(testNode) {
var nn = _nodeName(testNode);
return nn == "li" || nn == "ol" || nn == "ul";
}, 1);
// Found a list item / list container in the given range?
if (ca) {
// Get all list items in the given range
var listItems = [];
if (_nodeName(ca) == "li")
listItems.push(ca);
else {
_visitAllNodes(ca, startNode, true, function(domNode){
if (_nodeName(domNode) == "li") listItems.push(domNode);
return domNode != endNode;
});
// The traversal above will skip the start node's li
var li = _findAncestor(startNode, ca, function(testNode) {return _nodeName(testNode) == "li";}, 1);
if (li)
listItems.push(li);
}
// TODO: REFACTOR: ALL ABOVE IS IDENTICLE TO PROMOTE ACTION
// Demote LI: Top => down
for (var i in listItems) {
demoteLI(listItems[i]);
}
}
t.selAfter = t.selBefore;
function demoteLI(li){
// Can this list item be demoted? Its list container must be within a list container
var targetLICon = li.parentNode.parentNode;
if (_nodeName(targetLICon) == "ul" || _nodeName(targetLICon) == "ol") {
// Is there a following list item at the same level
var nextLI = li.nextSibling, prevLI = li.previousSibling; // lis, uls or ols,
while (prevLI && prevLI.nodeType == Node.TEXT_NODE) {
prevLI = prevLI.previousSibling;
}
while (nextLI && nextLI.nodeType == Node.TEXT_NODE) {
nextLI = nextLI.nextSibling;
}
var insertIndex;
if (nextLI && prevLI) { // List item to demote surrounded by others
// Split the contain in two
var lowerSplit = li.parentNode.cloneNode(false);
_execOp(_Operation.INSERT_NODE, lowerSplit, targetLICon, _indexInParent(li.parentNode) + 1);
// Move following list items into split
while (nextLI) {
var migrant = nextLI;
nextLI = nextLI.nextSibling;
_execOp(_Operation.REMOVE_NODE, migrant);
_execOp(_Operation.INSERT_NODE, migrant, lowerSplit);
}
insertIndex = _indexInParent(lowerSplit);
} else if (nextLI) { // List item to demote contains another afterwards
insertIndex = _indexInParent(li.parentNode);
} else if (prevLI) { // List item to demote contains another before
insertIndex = _indexInParent(li.parentNode) + 1;
} else { // List item to demote on its own
insertIndex = _indexInParent(li.parentNode);
// Remove the container
_execOp(_Operation.REMOVE_NODE, li.parentNode);
}
// Remove the li
_execOp(_Operation.REMOVE_NODE, li);
// Add it to the parent container
_execOp(_Operation.INSERT_NODE, li, targetLICon, insertIndex);
}
}
}
});
//End DemoteItemAction.js
//Start CreateTableAction.js
(function() {
_registerAction("CreateTable", {
exec : function(refNode, isBefore, rows, columns, cellspacing, cellpadding) {
// TODO
var markup = "
";
}
});
})();
//End CreateTableAction.js
//Start ChangeContainerAction.js
(function() {
var changeBlockMap = $createLookupMap("p,pre,h1,h2,h3,h4,h5,h6,address");
_registerAction("ChangeContainer", {
/**
* An undoable action. Changes all block level elements in a given range - creates new containers where needed.
*
* @author Brook Novak
*
* @param {String} containerTag (Optional) The container nodename to encapsulate the range.
*
* @param {Node} startNode (Optional) The starting dom node of the range to align.
* If not provided then the current selection will be used.
* If provieded must also provide endNode
*
* @param {Node} endNode (Optional) The ending dom node of the range to align. Can be the same as start node
* If not provided then the current selection will be used.
*
*/
exec : function(containerTag, startNode, endNode) {
if (!containerTag)
containerTag = "p";
containerTag = containerTag.toLowerCase();
debug.assert(changeBlockMap[containerTag], "Cannot change container type to \"" + changeBlockMap + "\"");
// Auto-set range if not provided.
if (!startNode) {
if (!this.selBefore)
return; // Nothing to select
if (this.selBefore.endNode) {
startNode = this.selBeforeOrdered.startNode;
endNode = this.selBeforeOrdered.endNode;
} else
startNode = endNode = this.selBefore.startNode;
}
debug.assert(endNode, "Supplied start node but not the end node");
var containers = _getNormalizedContainerRange(startNode, endNode);
for (var i in containers) {
var container = containers[i];
// Can this block level container be changed to a certain type?
if (_nodeName(container) != containerTag && changeBlockMap[_nodeName(container)]) {
var migrants = [],
migrant,
parent = container.parentNode,
index = _indexInParent(container);
// Remove container contents
while(migrant = container.firstChild) {
migrants.push(migrant);
_execOp(_Operation.REMOVE_NODE, migrant);
}
// Remove container
_execOp(_Operation.REMOVE_NODE, container);
// Insert new container
var newContainer = $createElement(containerTag);
_execOp(_Operation.INSERT_NODE, newContainer, parent, index);
// Migrate original container elements
for (var j in migrants) {
_execOp(_Operation.INSERT_NODE, migrants[j], newContainer);
}
}
}
this.selAfter = this.selBefore;
}
});
})();
//End ChangeContainerAction.js
/* BlockQuoteAction.js */
_registerAction("Blockquote",
{
/**
* An undoable blockquote action. Encapsulates a range with a
* block quote. If there is any block quotes within the range,
* or if the range is within a block quote, then the block quote
* will be removed instead.
*
* @author Brook Novak
*
* @param {Node}
* startNode (Optional) The starting dom node of the
* range to align. If not provided then the current
* selection will be used. If provieded must also
* provide endNode
*
* @param {Node}
* endNode (Optional) The ending dom node of the
* range to align. Can be the same as start node If
* not provided then the current selection will be
* used.
*
*/
exec : function(startNode, endNode) {
// Auto-set range if not provided.
if (!startNode) {
if (!this.selBefore)
return; // Nothing to select
if (this.selBefore.endNode) {
startNode = this.selBeforeOrdered.startNode;
endNode = this.selBeforeOrdered.endNode;
} else
startNode = endNode = this.selBefore.startNode;
}
debug.assert(endNode,
"Supplied start node but not the end node");
// Is there a block quote in the given range?
var bq = null, ca = _getCommonAncestor(startNode, endNode,
true);
if (isBlockQuote(ca))
bq = ca;
else {
_visitAllNodes(ca, startNode, true, function(domNode) {
if (isBlockQuote(domNode))
bq = domNode;
return bq == null && domNode != endNode;
});
}
// Is the range inside a block quote?
if (!bq) {
bq = _findAncestor(ca, docBody, isBlockQuote, true)
|| _findAncestor(startNode, ca, isBlockQuote,
true); // Initial traversal will have
// missed these nodes
// Check that looking outside range was ok (not
// venturing outside editable area)
if (!de.doc.isNodeEditable(bq))
bq = null;
}
if (bq) {
// Move block quote children outside of block quote
while (bq.firstChild) {
var migrant = bq.firstChild;
_execOp(_Operation.REMOVE_NODE, migrant);
_execOp(_Operation.INSERT_NODE, migrant,
bq.parentNode, _indexInParent(bq));
}
// Remove the block quote
_execOp(_Operation.REMOVE_NODE, bq);
} else { // Encapsulate range with a block quote
// Normalize containers in range and get list of all the
// containers
var containers = _getNormalizedContainerRange(
startNode, endNode);
var newbq = $createElement("blockquote");
if (containers.length > 0
&& _isValidRelationship(newbq,
containers[0].parentNode)) {
// Add the new block quote to the document
_execOp(_Operation.INSERT_NODE, newbq,
containers[0].parentNode,
_indexInParent(containers[0]));
// Migrate containers into block quote
for ( var i in containers) {
var con = containers[i];
_execOp(_Operation.REMOVE_NODE, con);
_execOp(_Operation.INSERT_NODE, con, newbq);
}
}
}
this.selAfter = this.selBefore;
function isBlockQuote(domNode) {
return _nodeName(domNode) == "blockquote";
}
}
});
/* DTDUtil.js */
var _HTML_401_MAPS = {
/* Generic block level */
GB : "address,blockquote,center,del,div,h1,h2,h3,h4,h5,h6,hr,ins,isindex,noscript,p,pre",
/* Special inline level */
SI : "a,applet,basefont,bdo,br,font,iframe,img,map,area,object,param,q,script,span,sub,sup",
/* Phrase level */
PH : "abbr,acronym,cite,code,del,dfn,em,ins,kbd,samp,strong,var",
/* Font level */
F : "b,big,i,s,small,strike,tt,u",
/* Table elements */
TE : "table,caption,colgroup,col,thead,tfoot,tbody,tr,td,th",
/* Form elements */
// FE :
// "form,button,fieldset,legend,input,label,select,optgroup,option,textarea",
/*
* Elements which to do support non-whitespace text as immediate
* children.
*/
NT : "table,textarea,tr,thead,tbody,tfoot,dl,ul,ol,menu,select,optgroup,option,script,style"
};
// Build maps
for ( var mem in _HTML_401_MAPS) {
_HTML_401_MAPS[mem] = $createLookupMap(_HTML_401_MAPS[mem]);
}
$extend(
_HTML_401_MAPS,
{
/* Block level */
B : $extend(
$createLookupMap("dir,dl,fieldset,form,menu,noframes,ol,table,ul,dd,dt,frameset,li,tbody,td,tfoot,thead,th,tr"),
_HTML_401_MAPS.GB),
/* Inline level */
I : $extend(
$extend(
$createLookupMap("abbr,acronym,cite,code,dfn,em,kbd,samp,strong,var"),
_HTML_401_MAPS.F), _HTML_401_MAPS.SI)
});
/*
* A map of maps containing valid immediate child relationships according to
* HTML 4.01 transactional.
*/
var _HTML_401_VALIDATION_MAP = function() {
function cloneSubset(map, exclude) {
var arr = exclude.split(","), clone = _clone(map);
for ( var i in arr) {
delete clone[arr[i]];
}
return clone;
}
var BLOCKINLINEMAP = $extend(_clone(_HTML_401_MAPS.B), _HTML_401_MAPS.I);
return {
// Body
body : $extend($createLookupMap("script,ins,del"), BLOCKINLINEMAP),
// Generic block level
address : $extend($createLookupMap("p"), BLOCKINLINEMAP),
blockquote : BLOCKINLINEMAP,
centre : BLOCKINLINEMAP,
del : BLOCKINLINEMAP,
h1 : _HTML_401_MAPS.I,
h2 : _HTML_401_MAPS.I,
h3 : _HTML_401_MAPS.I,
h4 : _HTML_401_MAPS.I,
h5 : _HTML_401_MAPS.I,
h6 : _HTML_401_MAPS.I,
hr : {},
ins : BLOCKINLINEMAP,
isindex : {},
noscript : BLOCKINLINEMAP,
p : _HTML_401_MAPS.I,
pre : cloneSubset(_HTML_401_MAPS.I,
"img,object,applet,big,small,sub,sup,font,basefont"),
// Lists
dir : $createLookupMap("li"),
dl : $createLookupMap("dd,dt"),
dt : _HTML_401_MAPS.I,
dd : BLOCKINLINEMAP,
li : BLOCKINLINEMAP,
menu : $createLookupMap("li"),
ol : $createLookupMap("li"),
ul : $createLookupMap("li"),
// Tables
table : $createLookupMap("caption,col,colgroup,thead,tfoot,tbody"),
caption : _HTML_401_MAPS.I,
colgroup : $createLookupMap("col"),
col : {},
thead : $createLookupMap("tr"),
tfoot : $createLookupMap("tr"),
tbody : $createLookupMap("tr"),
tr : $createLookupMap("td,th"),
td : BLOCKINLINEMAP,
th : BLOCKINLINEMAP,
// Forms
form : cloneSubset(BLOCKINLINEMAP, "form"),
button : cloneSubset(BLOCKINLINEMAP,
"a,input,select,textarea,label,button,iframe,form,isindex,fieldset"),
fieldset : $extend($createLookupMap("legend"), BLOCKINLINEMAP),
legend : _HTML_401_MAPS.I,
input : {},
label : cloneSubset(_HTML_401_MAPS.I, "label"),
select : $createLookupMap("optgroup,option"),
optgroup : $createLookupMap("option"),
option : {},
textarea : {},
// Special inline elements
a : cloneSubset(_HTML_401_MAPS.I, "a"),
applet : $extend($createLookupMap("param"), BLOCKINLINEMAP),
basefont : {},
bdo : _HTML_401_MAPS.I,
br : {},
font : _HTML_401_MAPS.I,
iframe : BLOCKINLINEMAP,
img : {},
map : $extend($createLookupMap("area"), _HTML_401_MAPS.B),
area : {},
object : $extend($createLookupMap("param"), BLOCKINLINEMAP),
param : {},
q : _HTML_401_MAPS.I,
script : {},
span : _HTML_401_MAPS.I,
sub : _HTML_401_MAPS.I,
sup : _HTML_401_MAPS.I,
// Phrase level
abbr : _HTML_401_MAPS.I,
acroynm : _HTML_401_MAPS.I,
cite : _HTML_401_MAPS.I,
code : _HTML_401_MAPS.I,
dfn : _HTML_401_MAPS.I,
em : _HTML_401_MAPS.I,
kbd : _HTML_401_MAPS.I,
samp : _HTML_401_MAPS.I,
strong : _HTML_401_MAPS.I,
'var' : _HTML_401_MAPS.I,
// Font level
b : _HTML_401_MAPS.I,
big : _HTML_401_MAPS.I,
i : _HTML_401_MAPS.I,
s : _HTML_401_MAPS.I,
small : _HTML_401_MAPS.I,
strike : _HTML_401_MAPS.I,
tt : _HTML_401_MAPS.I,
u : _HTML_401_MAPS.I
};
}();
function _isNodeAtLevel(domNode, mapName) {
if (domNode.nodeType == Node.ELEMENT_NODE)
return _HTML_401_MAPS[mapName][_nodeName(domNode)] ? true : false;
return false;
}
/**
* Determines if an Element or Text node can be a parent of another given
* Element/Text/Body node. Note: the contents of text nodes are not
* analysed, the relationship is valid for the type, but if the text node
* contains non-whitespace symbols the relationship may not be valid.
*
* Only configured for HTML 4.01 trans .. TODO: Other specs
*
* @see _doesTextSupportNonWS
*
* @param {Node}
* child The child to test
* @param {Node}
* parent The parent of the child to test
* @return {Boolean} True if child can be a child of Parent according to
* HTML 4.0 Transactional specification
*/
function _isValidRelationship(child, parent) {
if (child.nodeType == Node.TEXT_NODE)
return true;
var vmap = _HTML_401_VALIDATION_MAP[_nodeName(parent)];
if (vmap)
return vmap[_nodeName(child)];
return true; // Allow any custom nodes to be added
}
/*
* TODO: SUPPORT OF OTHER DOC TYPES.
*/
/**
* @param {Node}
* domNode A dom node to test
* @return {Boolean} True iff domNode is a block level element.
*/
function _isBlockLevel(domNode) {
return _isNodeAtLevel(domNode, "B");
}
/**
* @param {Node}
* domNode A dom node to test
* @return {Boolean} True iff domNode is a block level element.
*/
function _isGenericBlockLevel(domNode) {
return _isNodeAtLevel(domNode, "GB");
}
/**
* @param {Node}
* domNode A dom node to test
* @return {Boolean} True iff domNode is a inline level element.
*/
function _isInlineLevel(domNode) {
return _isNodeAtLevel(domNode, "I");
}
/**
* @param {Node}
* domNode A dom node to test
* @return {Boolean} True iff domNode is a inline level element.
*/
function _isSpecialInlineLevel(domNode) {
return _isNodeAtLevel(domNode, "SI");
}
/**
* @param {Node}
* domNode A dom node to test
* @return {Boolean} True iff domNode is a phrase level element.
*/
function _isPhraseLevel(domNode) {
return _isNodeAtLevel(domNode, "PH");
}
/**
* @param {Node}
* domNode A dom node to test
* @return {Boolean} True iff domNode is a font level element.
*/
function _isFontLevel(domNode) {
return _isNodeAtLevel(domNode, "F");
}
/**
* @param {Node}
* domNode A dom node to test
* @return {Boolean} True iff domNode is a table element. E.G. "tr", "thead"
* or "table" elements
*/
function _isTableElement(domNode) {
return _isNodeAtLevel(domNode, "TE");
}
/*
* @param {Node} domNode A dom node to test @return {Boolean} True iff
* domNode is a form element. E.G. "input", "textarea" or "form" elements
*/
/*
* function _isFormElement(domNode) { return _isNodeAtLevel(domNode, "FE"); }
*/
/**
* @param {String}
* str A string
* @return {Boolean} True iff str contains nothing but whitespace
* charactors.
*/
function _isAllWhiteSpace(str) {
return /^[\t\n\r ]+$/.test(str);
}
/**
* @param {Node}
* textNode A text node which has a parent node.
* @return {Boolean} True iff the text node is allowed to contain non
* whitespace symbols.
*/
function _doesTextSupportNonWS(textNode) {
return !_isNodeAtLevel(textNode.parentNode, "NT");
}
/**
* Ready only. UTF encoding for the non breaking backspace entity
* (" ")'
*/
var _NBSP = _parseHTMLString(" ");
$extend(de, {
isBlock : _isBlockLevel
});
/* Viewport.js */
var _getViewportSize, _getScrollbarThickness, _getBodyOffset, _getDocumentScrollPos;
(function() {
var
recalcViewportPending = 1, cachedVPWidth, cachedVPHeight;
$enqueueInit("viewport", function() {
// On resize invalidate scrollbars and body offset measurements
_addHandler(window, "resize", function() {
recalcViewportPending = 1;
});
});
/**
* @param {Boolean}
* forceReCalc True to force a relcalculation of the viewport
* dimension. Otherwised it may return a cached version. Only
* need to set to true if calling within a onresize event
* (since cached values are re-evaluated on resize events).
*
* @return {Object} The size of the documents viewport - excluding
* scrollbars. {width, height}
*/
de.getViewPortSize = _getViewPortSize = function(forceRecalc) {
if (recalcViewportPending || forceRecalc)
reCalcViewport();
return {
width : cachedVPWidth,
height : cachedVPHeight
};
};
/**
* Recalculates the viewport dimensions
*/
function reCalcViewport() {
if (_browser == _Platform.IE && _browserVersion < 7) {
// IE 6 and below does not support fixed positioned elements and
// has troubles with
// 100% heights .. can just query doc element client area
cachedVPWidth = document.documentElement.clientWidth;
cachedVPHeight = document.documentElement.clientHeight; // TODO:
// For
// all
// IE's?
} else {
var measureDiv = $createElement("div");
_setFullStyle(measureDiv,
"position:fixed;top:0;left:0;width:100%;height:100%;border-style:none;margin:0");
docBody.appendChild(measureDiv);
cachedVPWidth = measureDiv.offsetWidth;
cachedVPHeight = measureDiv.offsetHeight;
docBody.removeChild(measureDiv);
}
recalcViewportPending = 0;
}
})();
/**
* @return {Object} The current scroll position of the document {top, left}.
*/
de.getDocumentScrollPos = _getDocumentScrollPos = function() {
var left = 0, top = 0;
// DOM Compliant?
if (docBody.scrollLeft || docBody.scrollTop) {
left = docBody.scrollLeft;
top = docBody.scrollTop;
} else if (window.pageYOffset || window.pageXOffset) {
left = window.pageXOffset;
top = window.pageYOffset;
} else if (document.documentElement.scrollLeft
|| document.documentElement.scrollTop) {
left = document.documentElement.scrollLeft;
top = document.documentElement.scrollTop;
}
return {
top : top,
left : left
};
}
/**
* Note: this does not include the browser toolbars/status bars etc.. just
* the document viewing area...
*
* @return {Object} the size of the documents window - i.e. including
* scrollbars. {width, height}
*/
function _getWindowSize() {
var width = 0, height = 0;
if (window.innerWidth || window.innerHeight) { // Most Browsers
width = window.innerWidth;
height = window.innerHeight;
} else if (document.documentElement.offsetWidth
|| document.documentElement.offsetWidth) { // IE
width = document.documentElement.offsetWidth;
height = document.documentElement.offsetHeight;
} else if (docBody.offsetWidth || docBody.offsetWidth) {
width = docBody.offsetWidth;
height = docBody.offsetWidth;
}
return {
width : width,
height : height
};
}
;
/* file: WhitespaceUtil.js */
var _consolidateWSSeqs, _normalizeNBSP, _convertWSToNBSP;
(function() {
/* Elements which can be physically separated by white space. */
var breakableElements = $createLookupMap("button,img,iframe,map,object"),
/*
* Inline elements which cannot be regarded as part of a whitespace
* sequence.
*/
nonWSInlineElements = $createLookupMap("br,button,img,iframe,map,object,select,textarea,applet");
/**
* This does not create any undoable operations.
*
* @param {Node}
* targetNode A node to convert all whitespaces to NBSP
* entities in text nodes which can support non whitespace
* and has normal whitespace breaking
*/
_convertWSToNBSP = function(targetNode) {
_visitTextNodes(targetNode, targetNode, true, function(textNode) {
if (_doesTextSupportNonWS(textNode)
&& getWSStyle(textNode) == "normal")
textNode.nodeValue = textNode.nodeValue.replace(
/[\t\n\r ]/g, _NBSP);
});
};
/**
* Consolidates white space. This creates undoable operations.
*
* @param {Node}
* targetNode The DOM node and all it's descendants to
* consolidate.
*
* @param {Boolean}
* extendRange If the first text node begins with whitespace,
* then the first whitespace sequence may start before the
* target node. If the last text node in the range ends with
* whitespace, then the last whitespace sequence may end
* after the target node. Set to true to allow consolidation
* outside of the target node, false will truncate whitespace
* sequences within the target node.
*
*/
_consolidateWSSeqs = function(targetNode, extendRange) {
// Get the first text node within target node - that is editable
var ftn;
_visitTextNodes(targetNode, targetNode, true, function(textNode) {
if (_doesTextSupportNonWS(textNode)
&& _nodeLength(textNode) > 0) {
ftn = textNode;
return false;
}
});
// If there are no text nodes then there is nothing to consolidate
if (!ftn)
return;
// If the first text node contains a whitespace... extend range
// backward...
// possibly before the targetnode... to ensure that all preceeding
// whitespace
// that is part of the first node/index sequence is included. May
// over estimate but
// that is ok.
var currentNode = targetNode;
var ignorePreceedingWS = false;
if (extendRange && _isAllWhiteSpace(ftn.nodeValue.charAt(0))) {
_visitAllNodes(null, ftn, false, function(domNode) {
// Skip start node
if (domNode == ftn)
return;
if (domNode.nodeType == Node.TEXT_NODE) {
// Text nodes that do not support not whitespace
// shouldn't be consolidated...
if (!_doesTextSupportNonWS(domNode))
return false;
// Adjust new node to start consolidating from
currentNode = domNode;
// If the text node contains a nonWS charactor then the
// range has been extended enough
if (!_isAllWhiteSpace(domNode.nodeValue)) {
// Set flag to ignore any preceeding whitespace at
// the starting node (see later)
ignorePreceedingWS = true;
return false;
}
}
// If the node is not inline, then WS sequences can't spill
// over these
else if (!_isInlineLevel(domNode))
return false;
});
}
var seenTargetNode = _isAncestor(targetNode, currentNode), currentIndex = 0;
// Keep traversing through the target node's descendants until all
// whitespace sequences are
// consolidated or completely removed
while (currentNode) {
// Get the next whitespace sequence
var seq = nextWSSequence(currentNode, currentIndex,
ignorePreceedingWS, targetNode, seenTargetNode, false,
false, extendRange);
ignorePreceedingWS = false;
seenTargetNode = seq.seenTargetNode;
currentNode = seq.resumeNode;
currentIndex = seq.resumeIndex;
// Was there a whitespace sequence? if so, and the sequence is
// not using "pre" wrapping then
// there might be something to consolidate
if (seq.startNode && getWSStyle(seq.startNode) != "pre") {
// If the whitespace sequence breaks two inline/text
// elements apart, then adjust the range
// so that it leaves one whitespace behind
if (isBreaker(seq.startNode, seq.startIndex, seq.endNode,
seq.endIndex)) {
// If the whitespace sequence is just one in length,
// then there is nothing to consolidate
if (seq.startNode == seq.endNode
&& seq.startIndex == (seq.endIndex - 1)) {
seq.startNode = null;
} else {
// Increment start node / index by one whitespace to
// leave one white space behind
// If the start index is larger/equal to the start
// nodes text length,
// the fragment range will include the start node,
// but exclude it from removal.
seq.startIndex++;
}
}
// Is there anything to consolidate?
if (seq.startNode) {
// Create the fragment and disconnect it from the
// document
var seqFrag = _buildFragment(_getCommonAncestor(
seq.startNode, seq.endNode, false),
seq.startNode, seq.startIndex, seq.endNode,
seq.endIndex);
seqFrag.disconnect();
// Keep the current node/index pointer updated
var updateTargetNode = currentNode == targetNode;
if (currentNode) {
var startFrag = seqFrag.getStartFragment(), endFrag = seqFrag
.getEndFragment(), updated = false, wasStartSplit = seqFrag
.wasStartSplit(), wasEndSplit = seqFrag
.wasEndSplit();
// Is this node the same as the start node of the
// fragment, and was the start node split?
if (currentNode == seq.startNode && wasStartSplit) {
debug
.assert(startFrag.getPreSplitNode() == seq.startNode);
debug
.assert(_nodeLength(seq.startNode) == seq.startIndex);
// Does the index need updating?
if (currentIndex >= _nodeLength(seq.startNode)) {
var remTextLen = _nodeLength(startFrag.node);
// Does the index fall in the removed range?
if (currentIndex < (_nodeLength(seq.startNode) + remTextLen))
// If adjusting left, then simply
// truncate the index to the end of the
// start node
currentIndex = _nodeLength(seq.startNode) - 1;
// Was both the end node AND start node
// split at the same node?.. and the
// node/index
// is pointing in the remaining text (right
// most)?
else if (currentNode == seq.endNode
&& wasEndSplit) {
// Adjust the node to become the
// remaining text
currentNode = endFrag
.getPostSplitNode()
// Set the index to become relative to
// the split end node
currentIndex -= (_nodeLength(seq.startNode) + remTextLen);
} else
assert(false);
}
updated = true;
// Otherwise is this node the same as the end
// node of the fragment, and was the end node
// split?
} else if (currentNode == seq.endNode
&& wasEndSplit) {
var remTextLen = _nodeLength(endFrag.node);
// Does the index fall outside the removed
// range?
if (currentIndex >= remTextLen) {
// Adjust the node to become the remaining
// text
currentNode = endFrag.getPostSplitNode();
// Set the index to become relative to the
// split end node
currentIndex -= remTextLen;
updated = true;
// If not, then the node/index should be set
// to the start or end bounds node/index
} else
currentNode = null;
}
if (!updated) {
// Determine if the disconnection of the
// fragment removed this dom node
var wasRemoved;
if (currentNode) {
wasRemoved = false;
seqFrag.visit(function(frag) {
if (!frag.isShared
&& frag.node == currentNode) {
wasRemoved = true;
return false;
}
});
} else
wasRemoved = true;
if (wasRemoved) {
// If the node was removed, then set the
// node/index to the starting bounds
var frag = startFrag;
while (!frag.isShared) {
frag = frag.parent;
}
// Set the node to become the first shared
// node on the starting bound...
currentNode = frag.node;
// The index should be at the end of the
// start bound if the very-end of start
// bound still remains in the document,
// Otherwise the index should be set to the
// beggining of the start bound.
// It is possible for the very-end of the
// start fragment to still be included
// because if the
// sequence is a breaker, then the start
// index can be incremented exclude the
// start node.
currentIndex = frag == startFrag ? _nodeLength(
currentNode, 1)
: 0;
// If the shared node contains child nodes,
// then set the current node to become the
// child at which the startbounds
// proceeded from
if (currentNode.childNodes.length > 0
&& frag.children.length > 0
&& frag.children[0].pos > 0) {
currentNode = currentNode.childNodes[frag.children[0].pos - 1];
// Set index to the end of selected node
currentIndex = _nodeLength(currentNode,
1);
}
}
}
// If the current node is the same as the target
// node, the target node
// is a text node that has been split - so update
// this aswell
if (currentNode && updateTargetNode)
targetNode = currentNode;
}
}
}
} // End loop: consolidating whitespaces in target node
};
/**
* Converts any NBSP entities within a given node (and in some cases
* just outside of the node) into whitespace, only if the conversion
* won't collapse the whitespace.
*
* This will not create any undoable operations
*
* @param {Node}
* targetNode The node to normalize all containing non
* breaking spaces
*/
_normalizeNBSP = function(targetNode) {
var currentNode = targetNode, currentIndex = 0;
while (currentNode) {
// Get the next whitespace sequence.. including NBPS's
var seq = nextWSSequence(currentNode, currentIndex, false,
targetNode, true, true, true, true);
currentNode = seq.resumeNode;
currentIndex = seq.resumeIndex;
// Is there a whitespace sequence?
if (seq.startNode) {
var isWSSeqBreaker = isBreaker(seq.startNode,
seq.startIndex, seq.endNode, seq.endIndex), seqTextNodes = [];
// debug.println("Found ws sequence - wordbreaker=" +
// isWSSeqBreaker + ", endIndex=" + seq.endIndex);
// Get all text nodes in the whitespace sequence into an
// array
_visitTextNodes(_getCommonAncestor(seq.startNode,
seq.endNode, false), seq.startNode, true, function(
textNode) {
seqTextNodes.push(textNode);
if (textNode == seq.endNode)
return false;
});
// For each text node in the whitespace sequence....
for (var i = 0; i < seqTextNodes.length; i++) {
var textNode = seqTextNodes[i];
// For each charactor in the whitespace sequence
for (var index = (i == 0 ? seq.startIndex : 0); index < (i == (seqTextNodes.length - 1) ? seq.endIndex
: _nodeLength(textNode)); index++) {
// debug.println("Checking whitespace at index " +
// index + "...");
if (textNode.nodeValue.charAt(index) == _NBSP) {
// debug.println("Found NBSP at index " +
// index);
// Keep NBSP if the NBSP is at the start or end
// of the sequence, and the sequence is not
// a word breaker
if (!(!isWSSeqBreaker && ((i == 0 && index == seq.startIndex) || (i == (seqTextNodes.length - 1) && index == (seq.endIndex - 1))))) {
// Keep the NBSP if preceded by a whitespace
var ch;
if (index == 0)
ch = (i > 0) ? seqTextNodes[i - 1].nodeValue
.charAt(_nodeLength(seqTextNodes[i - 1]) - 1)
: null;
else
ch = textNode.nodeValue
.charAt(index - 1);
if (!ch || !_isAllWhiteSpace(ch)) {
// Keep the NBSP if proceeded by a
// whitespace
if (index == (_nodeLength(textNode) - 1))
ch = (i < (seqTextNodes.length - 1)) ? seqTextNodes[i + 1].nodeValue
.charAt(0)
: null;
else
ch = textNode.nodeValue
.charAt(index + 1);
// debug.println("ch = " + (ch ? ch :
// "NULL"));
if (!ch || !_isAllWhiteSpace(ch)) {
// Otherwise... replace the non
// breaking space with a whitespace
// debug.println("Replacing NBSP at
// index " + index + " (node length
// = " + _nodeLength(textNode) +
// ")");
textNode.nodeValue = textNode.nodeValue
.substr(0, index)
+ " "
+ textNode.nodeValue
.substr(index + 1);
}
}
}
}
} // End loop: iterating over whitespaces in ws
// seqence
} // End loop: Iterating over text nodes in ws sequence
}
} // End loop: searching for whitespace sequences in target node
};
/**
* Discovers the start and end points of the next whitespace seqeunce
* from a given point (inclusive)
*
* @param {Node}
* initNode The node to search from (towards the right)
* @param {Number}
* initIndex The index to search from.
* @param {Boolean}
* ignorePreceedingWS True to ignore the initial whitespaces
* encountered
* @param {Node}
* targetNode The node at which the search should reside
* within.
* @param {Boolean}
* seenTargetNode Flag as true if the target node has been
* visited.
* @param {Boolean}
* includeNBSP True to include non breaking spaces as
* whitespace, false to only count whitespace.
* @param {Boolean}
* ignoreInternalSingleWS True to ignore any single
* whitespace sequences that are definatly breaking two words
* apart
* @param {Boolean}
* extendRange True to allow the sequences to go past the
* target node for boundry cases.
*
* @return {Object} An object with the following members: seenTargetNode -
* true if the target node was encountered. resumeNode - The
* node to resume the search for remaining ws sequences in the
* target node resumeIndex - The index to resume the search for
* remaining ws sequences in the target node startNode - The
* start node of the sequence. Null if there was none.
* startIndex - The start index of the sequence (if there was
* one) endNode - The end node of the sequence, if there was
* one. endIndex - The end index of the sequence, if there was
* one.
*/
function nextWSSequence(initNode, initIndex, ignorePreceedingWS,
targetNode, seenTargetNode, includeNBSP,
ignoreInternalSingleWS, extendRange) {
var resumeNode = null, resumeIndex = initIndex, startNode, startIndex, endNode, endIndex, startWSStyle, curWSStyle;
// Locate the next whitespace sequence from the current node onwards
// (if any).
_visitAllNodes(
null,
initNode,
true,
function(domNode) {
// Has the search space exhausted? I.E: Has the
// traversal gone past the target node's descendants -
// and at this point isn't looking for any whitespace to
// consolidate?
if (seenTargetNode && domNode != targetNode
&& (!startNode || !extendRange)
&& !_isAncestor(targetNode, domNode)) // Case:
// if
// target
// is
// text
// node,
// then
// it
// can
// split...
// and
// prematurely
// end
// search
return false; // Finished consolidating/removing
// ws
// Update flag if domnode is the target node
seenTargetNode |= (domNode == targetNode);
// Set helper: the whitespace CSS style for the current
// visited dom node
curWSStyle = getWSStyle(domNode);
if (startNode) {
// If building a whitespace sequence, check to see
// if the ancestors of the starting node - up to
// the common ancestor of the start node and this
// current node - can be contained in a whitespace
// sequence.
var ca = _getCommonAncestor(domNode, startNode,
false);
var ancestors = _getAncestors(startNode, ca, false,
false);
var terminateSeq = false;
for ( var i in ancestors) {
if (!(_isInlineLevel(ancestors[i]) && !nonWSInlineElements[_nodeName(ancestors[i])])) {
terminateSeq = true;
break;
}
}
// Whitespace sequences cannot contain different
// breaking mechanisms.
if (terminateSeq || curWSStyle != startWSStyle) {
resumeNode = domNode;
resumeIndex = 0;
return false;
}
}
if (domNode.nodeType == Node.TEXT_NODE) {
if (domNode.parentNode.nodeType != Node.COMMENT_NODE) {
if (!_doesTextSupportNonWS(domNode)) {
debug.assert(!ignorePreceedingWS);
// If there is potentially something to
// consolidate, abort this traversal
if (startNode) {
// Record current position to resume
// traversal after consolidation
resumeNode = domNode;
resumeIndex = 0;
return false;
}
} else {
// Iterate over charactors in the text run
while (resumeIndex < _nodeLength(domNode)) {
var ch = domNode.nodeValue
.charAt(resumeIndex);
if (_isAllWhiteSpace(ch)
|| (includeNBSP && ch == _NBSP)) {
if (!ignorePreceedingWS) {
// Note start/end node/index of
// whitespace sequence
if (startNode) {
endNode = domNode;
endIndex = resumeIndex + 1;
} else {
startNode = domNode;
startWSStyle = curWSStyle;
startIndex = resumeIndex;
endNode = null;
}
}
} else { // Non whitespace charactor
ignorePreceedingWS = false;
// Is there a current sequence that
// has more than 1 whitespace, or
// one that resides at the start of
// this text run?
if (endNode
|| (startNode && (ignoreInternalSingleWS || startIndex == 0))) {
// Record current position to
// resume traversal after
// consolidation
resumeNode = domNode;
return false;
// Ignore any previous
// single-whitespace sequences,
// that do not reside at the
// start of the text run
} else
startNode = null;
}
resumeIndex++;
} // End loop: iterating over charactors
// in text run
}
} else
ignorePreceedingWS = false;
} else { // Not a text node
ignorePreceedingWS = false;
if (domNode.nodeType != Node.COMMENT_NODE) {
// Whitespace sequences can contain a subset of
// inline elements.
if (startNode
&& !(_isInlineLevel(domNode) && !nonWSInlineElements[_nodeName(domNode)])) {
resumeNode = domNode;
resumeIndex = 0;
return false;
}
}
// The element at this point can be part of the
// current whitespace sequence...
}
resumeIndex = 0;
}); // End visit
// If sequence is one in length, must set end position
if (startNode && !endNode) {
endNode = startNode;
endIndex = startIndex + 1;
}
return {
seenTargetNode : seenTargetNode,
resumeNode : resumeNode,
resumeIndex : resumeIndex,
startNode : startNode,
startIndex : startIndex,
endNode : endNode,
endIndex : endIndex
};
}
/**
* @param {Node}
* startNode The starting text node of the whitespace
* sequence
* @param {Number}
* startIndex The starting index of the whitespace sequence
* @param {Node}
* endNode The ending text node of the whitespace sequence
* @param {Number}
* endIndex The ending index of the whitespace sequence
* @return {Boolean} True iff the given whitespace sequence breaks two
* words/breakable-elements apart.
*/
function isBreaker(startNode, startIndex, endNode, endIndex) {
var startWSStyle = getWSStyle(startNode);
// Look to the left
if (startIndex == 0) {
var found = false;
_visitAllNodes(null, startNode, false,
function(domNode) {
if (domNode == startNode)
return; // Skip initial text node
var res = scan(domNode, startNode);
if (!res && found) {
// Check that all ancestors up to and excluding
// the common ancestor of this dom node
// and the start node, are all nodes which are
// breaked by whitespace
var ca = _getCommonAncestor(domNode, startNode,
false);
var ancestors = _getAncestors(domNode, ca,
false, false);
for ( var i in ancestors) {
// Reset found flag
found = false;
// Check ancestor if it does not break on
// whitespace...
if (!scan(ancestors[i], startNode)
&& !found)
return false;
}
// Restore flags
res = false;
found = true;
}
return res;
});
if (!found)
return false;
}
// Look to the right
if (endIndex == _nodeLength(endNode)) {
var found = false;
_visitAllNodes(null, endNode, true,
function(domNode) {
if (domNode == endNode)
return; // Skip initial text node
var res = scan(domNode, endNode);
if (!res && found) {
// Check that all ancestors up to and excluding
// the common ancestor of this dom node
// and the end node, are all nodes which are
// breakable by whitespace
var ca = _getCommonAncestor(domNode, endNode,
false);
var ancestors = _getAncestors(endNode, ca,
false, false);
for ( var i in ancestors) {
// Reset found flag
found = false;
// Check ancestor if it does not break on
// whitespace...
if (!scan(ancestors[i], endNode) && !found)
return false;
}
// Restore flags
found = true;
res = false;
}
return res;
});
if (!found)
return false;
}
return true;
/**
* Inner helper function.
*
* Sets the "found" local to true if domnode is considered breakable
* (in it's context)
*
* @param {Node}
* domNode The node to check
* @param {Node}
* initialNode The end or start node of the scan
* @return {Boolean} True to continue scanning, false to abort...a
* result was found.
*/
function scan(domNode, initialNode) {
if (domNode.nodeType == Node.TEXT_NODE) {
if (_nodeLength(domNode) > 0) {
found = _doesTextSupportNonWS(domNode); // Non-WS nodes
// are not
// breakable
return false;
}
} else if (breakableElements[_nodeName(domNode)]) {
found = !_isAncestor(domNode, initialNode); // WS Doesn't
// break from
// within
// breakable
// nodes to
// outside of
// them
return false;
// If hit a block level element or line break before a
// breakable node, then the sequence must be
// leading or trailing text.
} else if (_isBlockLevel(domNode) || _nodeName(domNode) == "br")
return false;
// Keep looking...
return true;
}// End inner scan
}
/**
* @param {Node}
* node A node to get it's whitespace CSS style for.
* @return {String} The CSS white-space style for the given node, never
* null/always is a style.
*/
function getWSStyle(node) {
var style = _getComputedStyle(node, "white-space");
if (!style) {
// Check if descends from PRE
do {
if (_nodeName(node) == "pre") {
style = "pre";
break;
}
node = node.parentNode;
} while (node && node.nodeType == Node.ELEMENT_NODE);
// Set as normal
if (!style)
style = "normal";
}
return style;
}
})()
})();