/* * 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; } } } } }; })();