/*
* file: Selection.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
*/
bootstrap.provides("Selection");
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;
}
}
}
}
};
})();