| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| // FIXME: cursor does not display correctly if it is after a space at the end of the line |
| |
| var Selection_isMarked; |
| var Selection_get; |
| var Selection_set; |
| var Selection_clear; |
| |
| var Selection_update; |
| var Selection_selectAll; |
| var Selection_selectParagraph; |
| var Selection_selectWordAtCursor; |
| var Selection_dragSelectionBegin; |
| var Selection_dragSelectionUpdate; |
| var Selection_moveStartLeft; |
| var Selection_moveStartRight; |
| var Selection_moveEndLeft; |
| var Selection_moveEndRight; |
| var Selection_setSelectionStartAtCoords; |
| var Selection_setSelectionEndAtCoords; |
| var Selection_setTableSelectionEdgeAtCoords; |
| var Selection_setEmptySelectionAt; |
| var Selection_deleteRangeContents; |
| var Selection_deleteContents; |
| var Selection_clearSelection; |
| var Selection_preserveWhileExecuting; |
| var Selection_posAtStartOfWord; |
| var Selection_posAtEndOfWord; |
| var Selection_preferElementPositions; |
| var Selection_print; |
| |
| (function() { |
| |
| var HANDLE_NONE = 0; |
| var HANDLE_START = 1; |
| var HANDLE_END = 2; |
| |
| var activeHandle = HANDLE_NONE; |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////// |
| // // |
| // Selection getter and setter // |
| // // |
| //////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| var Selection_setInternal; |
| |
| (function() { |
| |
| var selection = new Object(); |
| |
| Selection_isMarked = function() |
| { |
| if (selection.value == null) |
| return null; |
| else |
| return selection.value.isMarked; |
| } |
| |
| // public |
| Selection_get = function() |
| { |
| if (selection.value == null) |
| return null; |
| else |
| return new Range(selection.value.startNode,selection.value.startOffset, |
| selection.value.endNode,selection.value.endOffset); |
| } |
| |
| // public |
| Selection_setInternal = |
| function(newStartNode,newStartOffset,newEndNode,newEndOffset,isMarked) |
| { |
| var range = new Range(newStartNode,newStartOffset,newEndNode,newEndOffset); |
| if (!Range_isForwards(range)) |
| range = new Range(newEndNode,newEndOffset,newStartNode,newStartOffset); |
| range = boundaryCompliantRange(range); |
| |
| UndoManager_setProperty(selection,"value", |
| { startNode: range.start.node, |
| startOffset: range.start.offset, |
| endNode: range.end.node, |
| endOffset: range.end.offset, |
| isMarked: isMarked }); |
| } |
| |
| Selection_set = function(newStartNode,newStartOffset,newEndNode,newEndOffset, |
| keepActiveHandle,isMarked) |
| { |
| Selection_setInternal(newStartNode,newStartOffset,newEndNode,newEndOffset,isMarked); |
| Selection_update(); |
| if (!keepActiveHandle) |
| activeHandle = HANDLE_NONE; |
| } |
| |
| // public |
| Selection_clear = function() |
| { |
| UndoManager_setProperty(selection,"value",null); |
| Selection_update(); |
| } |
| })(); |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////// |
| // // |
| // Other selection functions // |
| // // |
| //////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| var selectionDivs = new Array(); |
| var selectionHighlights = new Array(); |
| var tableSelection = null; |
| |
| // private |
| updateTableSelection = function(selRange) |
| { |
| tableSelection = Tables_regionFromRange(selRange); |
| if (tableSelection == null) |
| return false; |
| |
| Range_trackWhileExecuting(selRange,function() { |
| |
| removeSelectionHighlights(getRangeData(null)); |
| |
| var sel = tableSelection; |
| |
| var topLeftTD = Table_get(sel.structure,sel.top,sel.left); |
| var bottomRightTD = Table_get(sel.structure,sel.bottom,sel.right); |
| |
| var topLeftRect = topLeftTD.element.getBoundingClientRect(); |
| var bottomRightRect = bottomRightTD.element.getBoundingClientRect(); |
| |
| var left = topLeftRect.left; |
| var top = topLeftRect.top; |
| |
| var bottom = bottomRightRect.bottom; |
| var right = bottomRightRect.right; |
| |
| var x = left; |
| var y = top; |
| var width = right - left; |
| var height = bottom - top; |
| |
| x += window.scrollX; |
| y += window.scrollY; |
| |
| var div = makeSelectionDiv(); |
| DOM_setAttribute(div,"class",Keys.SELECTION_HIGHLIGHT); |
| DOM_setStyleProperties(div,{ "position": "absolute", |
| "left": x+"px", |
| "top": y+"px", |
| "width": width+"px", |
| "height": height+"px", |
| "background-color": "rgb(201,221,238)", |
| "z-index": -1 }); |
| |
| setTableEdges(x,y,width,height); |
| setEditorHandles({ type: "table", x: x, y: y, width: width, height: height }); |
| }); |
| |
| Selection_setInternal(selRange.start.node,selRange.start.offset, |
| selRange.end.node,selRange.end.offset); |
| |
| return true; |
| } |
| |
| function makeSelectionDiv() |
| { |
| var div = DOM_createElement(document,"DIV"); |
| DOM_appendChild(document.body,div); |
| selectionDivs.push(div); |
| return div; |
| } |
| |
| function setTableEdges(x,y,width,height) |
| { |
| var left = makeSelectionDiv(); |
| var right = makeSelectionDiv(); |
| var top = makeSelectionDiv(); |
| var bottom = makeSelectionDiv(); |
| |
| var thick = 2; |
| width++; |
| height++; |
| setBoxCoords(left,x-thick,y-thick,thick,height+2*thick); |
| setBoxCoords(right,x+width,y-thick,thick,height+2*thick); |
| setBoxCoords(top,x-thick,y-thick,width+2*thick,thick); |
| setBoxCoords(bottom,x-thick,y+height,width+2*thick,thick); |
| |
| function setBoxCoords(box,x,y,width,height) |
| { |
| DOM_setStyleProperties(box,{ "position": "absolute", |
| "left": x+"px", |
| "top": y+"px", |
| "width": width+"px", |
| "height": height+"px", |
| "background-color": "blue", |
| "z-index": 1 }); |
| } |
| } |
| |
| var editorHandles = { type: "none" }; |
| function setEditorHandles(info) |
| { |
| var oldEditorHandles = editorHandles; |
| editorHandles = info; |
| UndoManager_addAction(function() { |
| setEditorHandles(oldEditorHandles); |
| }); |
| if (info.type == "cursor") { |
| Editor_setCursor(info.left,info.top,info.width,info.height); |
| } |
| else if (info.type == "selection") { |
| if (!Selection_isMarked()) { |
| Editor_setSelectionHandles(info.x1,info.y1, |
| info.height1,info.x2,info.y2,info.height2); |
| } |
| Editor_setSelectionBounds(info.boundsLeft,info.boundsTop, |
| info.boundsRight,info.boundsBottom); |
| } |
| else if (info.type == "none") { |
| Editor_clearSelectionHandlesAndCursor(); |
| } |
| else if (info.type == "table") { |
| Editor_setTableSelection(info.x,info.y,info.width,info.height); |
| } |
| else { |
| throw new Error("setEditorHandles: unknown type "+type); |
| } |
| } |
| |
| function getPrevHighlightText(node) |
| { |
| if ((node.previousSibling != null) && |
| isSelectionHighlight(node.previousSibling) && |
| (node.previousSibling.lastChild != null) && |
| (node.previousSibling.lastChild.nodeType == Node.TEXT_NODE)) |
| return node.previousSibling.lastChild; |
| else |
| return null; |
| } |
| |
| function getNextHighlightText(node) |
| { |
| if ((node.nextSibling != null) && |
| isSelectionHighlight(node.nextSibling) && |
| (node.nextSibling.firstChild != null) && |
| (node.nextSibling.firstChild.nodeType == Node.TEXT_NODE)) |
| return node.nextSibling.firstChild; |
| else |
| return null; |
| } |
| |
| function getTextNodeBefore(node) |
| { |
| var prev = node.previousSibling; |
| if ((prev != null) && (prev.nodeType == Node.TEXT_NODE)) { |
| return prev; |
| } |
| else { |
| var text = DOM_createTextNode(document,""); |
| DOM_insertBefore(node.parentNode,text,node); |
| return text; |
| } |
| } |
| |
| function getTextNodeAfter(node) |
| { |
| var next = node.nextSibling; |
| if ((next != null) && (next.nodeType == Node.TEXT_NODE)) { |
| return next; |
| } |
| else { |
| var text = DOM_createTextNode(document,""); |
| DOM_insertBefore(node.parentNode,text,node.nextSibling); |
| return text; |
| } |
| } |
| |
| function setSelectionHighlights(highlights) |
| { |
| UndoManager_addAction(setSelectionHighlights,selectionHighlights); |
| selectionHighlights = highlights; |
| } |
| |
| function createSelectionHighlights(data) |
| { |
| var newHighlights = arrayCopy(selectionHighlights); |
| |
| var outermost = data.outermost; |
| for (var i = 0; i < outermost.length; i++) { |
| recurse(outermost[i]); |
| } |
| |
| setSelectionHighlights(newHighlights); |
| |
| function recurse(node) |
| { |
| if (isSpecialBlockNode(node)) { |
| if (!isSelectionHighlight(node.parentNode)) { |
| var wrapped = DOM_wrapNode(node,"DIV"); |
| DOM_setAttribute(wrapped,"class",Keys.SELECTION_CLASS); |
| newHighlights.push(wrapped); |
| } |
| } |
| else if (isNoteNode(node)) { |
| if (!isSelectionHighlight(node.parentNode)) { |
| var wrapped = DOM_wrapNode(node,"SPAN"); |
| DOM_setAttribute(wrapped,"class",Keys.SELECTION_CLASS); |
| newHighlights.push(wrapped); |
| } |
| } |
| else if (node.nodeType == Node.TEXT_NODE) { |
| createTextHighlight(node,data,newHighlights); |
| } |
| else { |
| var next; |
| for (var child = node.firstChild; child != null; child = next) { |
| next = child.nextSibling; |
| recurse(child); |
| } |
| } |
| } |
| } |
| |
| function createTextHighlight(node,data,newHighlights) |
| { |
| var selRange = data.range; |
| if (isSelectionHighlight(node.parentNode)) { |
| |
| if ((node == selRange.end.node) && (node.nodeValue.length > selRange.end.offset)) { |
| var destTextNode = getTextNodeAfter(node.parentNode); |
| DOM_moveCharacters(node, |
| selRange.end.offset, |
| node.nodeValue.length, |
| destTextNode,0, |
| true,false); |
| } |
| if ((node == selRange.start.node) && (selRange.start.offset > 0)) { |
| var destTextNode = getTextNodeBefore(node.parentNode); |
| DOM_moveCharacters(node, |
| 0, |
| selRange.start.offset, |
| destTextNode,destTextNode.nodeValue.length, |
| false,true); |
| } |
| |
| return; |
| } |
| |
| var anext; |
| for (var a = node; a != null; a = anext) { |
| anext = a.parentNode; |
| if (isSelectionHighlight(a)) |
| DOM_removeNodeButKeepChildren(a); |
| } |
| |
| if (node == selRange.end.node) { |
| if (isWhitespaceString(node.nodeValue.substring(0,selRange.end.offset))) |
| return; |
| Formatting_splitTextAfter(selRange.end, |
| function() { return true; });a |
| } |
| |
| |
| if (node == selRange.start.node) { |
| if (isWhitespaceString(node.nodeValue.substring(selRange.start.offset))) |
| return; |
| Formatting_splitTextBefore(selRange.start, |
| function() { return true; }); |
| } |
| |
| var prevText = getPrevHighlightText(node); |
| var nextText = getNextHighlightText(node); |
| |
| if ((prevText != null) && containsSelection(data.nodeSet,prevText)) { |
| DOM_moveCharacters(node,0,node.nodeValue.length, |
| prevText,prevText.nodeValue.length,true,false); |
| DOM_deleteNode(node); |
| } |
| else if ((nextText != null) && containsSelection(data.nodeSet,nextText)) { |
| DOM_moveCharacters(node,0,node.nodeValue.length, |
| nextText,0,false,true); |
| DOM_deleteNode(node); |
| } |
| else if (!isWhitespaceTextNode(node)) { |
| // Call moveCharacters() with an empty range, to force any tracked positions |
| // that are at the end of prevText or the start of nextText to move into this |
| // node |
| if (prevText != null) { |
| DOM_moveCharacters(prevText, |
| prevText.nodeValue.length,prevText.nodeValue.length, |
| node,0); |
| } |
| if (nextText != null) { |
| DOM_moveCharacters(nextText,0,0,node,node.nodeValue.length); |
| } |
| |
| var wrapped = DOM_wrapNode(node,"SPAN"); |
| DOM_setAttribute(wrapped,"class",Keys.SELECTION_CLASS); |
| newHighlights.push(wrapped); |
| } |
| } |
| |
| function getRangeData(selRange) |
| { |
| var nodeSet = new NodeSet(); |
| var nodes; |
| var outermost; |
| if (selRange != null) { |
| outermost = Range_getOutermostNodes(selRange); |
| nodes = Range_getAllNodes(selRange); |
| for (var i = 0; i < nodes.length; i++) |
| nodeSet.add(nodes[i]); |
| } |
| else { |
| nodes = new Array(); |
| outermost = new Array(); |
| } |
| return { range: selRange, nodeSet: nodeSet, nodes: nodes, outermost: outermost }; |
| } |
| |
| function removeSelectionHighlights(data,force) |
| { |
| var selectedSet = data.nodeSet; |
| |
| var remainingHighlights = new Array(); |
| var checkMerge = new Array(); |
| for (var i = 0; i < selectionHighlights.length; i++) { |
| var span = selectionHighlights[i]; |
| if ((span.parentNode != null) && (force || !containsSelection(selectedSet,span))) { |
| if (span.firstChild != null) |
| checkMerge.push(span.firstChild); |
| if (span.lastChild != null) |
| checkMerge.push(span.lastChild); |
| |
| DOM_removeNodeButKeepChildren(span); |
| } |
| else if (span.parentNode != null) { |
| remainingHighlights.push(span); |
| } |
| } |
| setSelectionHighlights(remainingHighlights); |
| |
| for (var i = 0; i < checkMerge.length; i++) { |
| // if not already merged |
| if ((checkMerge[i] != null) && (checkMerge[i].parentNode != null)) { |
| Formatting_mergeWithNeighbours(checkMerge[i],{}); |
| } |
| } |
| } |
| |
| function containsSelection(selectedSet,node) |
| { |
| if (selectedSet.contains(node)) |
| return true; |
| for (var child = node.firstChild; child != null; child = child.nextSibling) { |
| if (containsSelection(selectedSet,child)) |
| return true; |
| } |
| return false; |
| } |
| |
| Selection_update = function() |
| { |
| var selRange = Selection_get(); |
| var selMarked = Selection_isMarked(); |
| |
| Range_trackWhileExecuting(selRange,function() { |
| // Remove table selection DIVs |
| for (var i = 0; i < selectionDivs.length; i++) |
| DOM_deleteNode(selectionDivs[i]); |
| selectionDivs = new Array(); |
| }); |
| |
| if (selRange == null) { |
| DOM_ignoreMutationsWhileExecuting(function() { |
| removeSelectionHighlights(getRangeData(null)); |
| }); |
| return; |
| } |
| |
| Range_assertValid(selRange,"Selection"); |
| |
| if (Range_isEmpty(selRange)) { |
| // We just have a cursor |
| |
| Range_trackWhileExecuting(selRange,function() { |
| DOM_ignoreMutationsWhileExecuting(function() { |
| removeSelectionHighlights(getRangeData(selRange)); |
| }); |
| }); |
| // Selection may have changed as a result of removeSelectionHighlights() |
| Selection_setInternal(selRange.start.node,selRange.start.offset, |
| selRange.end.node,selRange.end.offset, |
| selMarked); |
| selRange = Selection_get(); // since setInternal can theoretically change it |
| |
| // If we can't find the cursor rect for some reason, just don't update the position. |
| // This is better than using an incorrect position or throwing an exception. |
| var rect = Position_displayRectAtPos(selRange.end); |
| if (rect != null) { |
| var left = rect.left + window.scrollX; |
| var top = rect.top + window.scrollY; |
| var height = rect.height; |
| var width = rect.width ? rect.width : 2; |
| setEditorHandles({ type: "cursor", |
| left: left, |
| top: top, |
| width: width, |
| height: height}); |
| } |
| return; |
| } |
| |
| if (updateTableSelection(selRange)) |
| return; |
| |
| var rects = Range_getClientRects(selRange); |
| |
| if ((rects != null) && (rects.length > 0)) { |
| var boundsLeft = null; |
| var boundsRight = null; |
| var boundsTop = null; |
| var boundsBottom = null |
| |
| for (var i = 0; i < rects.length; i++) { |
| var left = rects[i].left + window.scrollX; |
| var top = rects[i].top + window.scrollY; |
| var width = rects[i].width; |
| var height = rects[i].height; |
| var right = left + width; |
| var bottom = top + height; |
| |
| if (boundsLeft == null) { |
| boundsLeft = left; |
| boundsTop = top; |
| boundsRight = right; |
| boundsBottom = bottom; |
| } |
| else { |
| if (boundsLeft > left) |
| boundsLeft = left; |
| if (boundsRight < right) |
| boundsRight = right; |
| if (boundsTop > top) |
| boundsTop = top; |
| if (boundsBottom < bottom) |
| boundsBottom = bottom; |
| } |
| } |
| |
| Range_trackWhileExecuting(selRange,function() { |
| DOM_ignoreMutationsWhileExecuting(function() { |
| var data = getRangeData(selRange); |
| createSelectionHighlights(data); |
| removeSelectionHighlights(data); |
| }); |
| }); |
| |
| // Selection may have changed as a result of create/removeSelectionHighlights() |
| Selection_setInternal(selRange.start.node,selRange.start.offset, |
| selRange.end.node,selRange.end.offset, |
| selMarked); |
| |
| var firstRect = rects[0]; |
| var lastRect = rects[rects.length-1]; |
| |
| var x1 = firstRect.left + window.scrollX; |
| var y1 = firstRect.top + window.scrollY; |
| var height1 = firstRect.height; |
| var x2 = lastRect.right + window.scrollX; |
| var y2 = lastRect.top + window.scrollY; |
| var height2 = lastRect.height; |
| |
| setEditorHandles({ type: "selection", |
| x1: x1, |
| y1: y1, |
| height1: height1, |
| x2: x2, |
| y2: y2, |
| height2: height2, |
| boundsLeft: boundsLeft, |
| boundsTop: boundsTop, |
| boundsRight: boundsRight, |
| boundsBottom: boundsBottom });; |
| |
| } |
| else { |
| setEditorHandles({ type: "none" }); |
| } |
| return; |
| |
| function getAbsoluteOffset(node) |
| { |
| var offsetLeft = 0; |
| var offsetTop = 0; |
| for (; node != null; node = node.parentNode) { |
| if (node.offsetLeft != null) |
| offsetLeft += node.offsetLeft; |
| if (node.offsetTop != null) |
| offsetTop += node.offsetTop; |
| } |
| return { offsetLeft: offsetLeft, offsetTop: offsetTop }; |
| } |
| } |
| |
| // public |
| Selection_selectAll = function() |
| { |
| Selection_set(document.body,0,document.body,document.body.childNodes.length); |
| } |
| |
| // public |
| Selection_selectParagraph = function() |
| { |
| var selRange = Selection_get(); |
| if (selRange == null) |
| return; |
| var startNode = Position_closestActualNode(selRange.start); |
| while (!isParagraphNode(startNode) && !isContainerNode(startNode)) |
| startNode = startNode.parentNode; |
| |
| var endNode = Position_closestActualNode(selRange.end); |
| while (!isParagraphNode(endNode) && !isContainerNode(endNode)) |
| endNode = endNode.parentNode; |
| |
| var startPos = new Position(startNode,0); |
| var endPos = new Position(endNode,DOM_maxChildOffset(endNode)); |
| startPos = Position_closestMatchForwards(startPos,Position_okForMovement); |
| endPos = Position_closestMatchBackwards(endPos,Position_okForMovement); |
| |
| Selection_set(startPos.node,startPos.offset,endPos.node,endPos.offset); |
| } |
| |
| // private |
| function getPunctuationCharsForRegex() |
| { |
| var escaped = "^$\\.*+?()[]{}|"; // From ECMAScript regexp spec (PatternCharacter) |
| var unescaped = ""; |
| for (var i = 32; i <= 127; i++) { |
| var c = String.fromCharCode(i); |
| if ((escaped.indexOf(c) < 0) && !c.match(/[\w\d]/)) |
| unescaped += c; |
| } |
| return unescaped + escaped.replace(/(.)/g,"\\$1"); |
| } |
| |
| // The following regular expressions are used by selectWordAtCursor(). We initialise them at |
| // startup to avoid repeatedly initialising them. |
| var punctuation = getPunctuationCharsForRegex(); |
| var wsPunctuation = "\\s"+punctuation; |
| |
| // Note: We use a blacklist of punctuation characters here instead of a whitelist of "word" |
| // characters, as the \w character class in javascript regular expressions only matches |
| // characters in english words. By using a blacklist, and assuming every other character is |
| // part of a word, we can select words containing non-english characters. This isn't a perfect |
| // solution, because there are many unicode characters that represent punctuation as well, but |
| // at least we handle the common ones here. |
| |
| var reOtherEnd = new RegExp("["+wsPunctuation+"]*$"); |
| var reOtherStart = new RegExp("^["+wsPunctuation+"]*"); |
| var reWordOtherEnd = new RegExp("[^"+wsPunctuation+"]*["+wsPunctuation+"]*$"); |
| var reWordOtherStart = new RegExp("^["+wsPunctuation+"]*[^"+wsPunctuation+"]*"); |
| |
| var reWordStart = new RegExp("^[^"+wsPunctuation+"]+"); |
| var reWordEnd = new RegExp("[^"+wsPunctuation+"]+$"); |
| |
| Selection_posAtStartOfWord = function(pos) |
| { |
| var node = pos.node; |
| var offset = pos.offset; |
| |
| if (node.nodeType == Node.TEXT_NODE) { |
| var before = node.nodeValue.substring(0,offset); |
| var matches = before.match(reWordEnd); |
| if (matches) { |
| var wordStart = offset - matches[0].length; |
| return new Position(node,wordStart); |
| } |
| } |
| |
| return pos; |
| } |
| |
| Selection_posAtEndOfWord = function(pos) |
| { |
| var node = pos.node; |
| var offset = pos.offset; |
| |
| if (node.nodeType == Node.TEXT_NODE) { |
| var after = node.nodeValue.substring(offset); |
| var matches = after.match(reWordStart); |
| if (matches) { |
| var wordEnd = offset + matches[0].length; |
| return new Position(node,wordEnd); |
| } |
| } |
| |
| return pos; |
| } |
| |
| function rangeOfWordAtPos(pos) |
| { |
| var node = pos.node; |
| var offset = pos.offset; |
| |
| if (node.nodeType == Node.TEXT_NODE) { |
| var before = node.nodeValue.substring(0,offset); |
| var after = node.nodeValue.substring(offset); |
| |
| var otherBefore = before.match(reOtherEnd)[0]; |
| var otherAfter = after.match(reOtherStart)[0]; |
| |
| var wordOtherBefore = before.match(reWordOtherEnd)[0]; |
| var wordOtherAfter = after.match(reWordOtherStart)[0]; |
| |
| var startOffset = offset; |
| var endOffset = offset; |
| |
| var haveWordBefore = (wordOtherBefore.length != otherBefore.length); |
| var haveWordAfter = (wordOtherAfter.length != otherAfter.length); |
| |
| if ((otherBefore.length == 0) && (otherAfter.length == 0)) { |
| startOffset = offset - wordOtherBefore.length; |
| endOffset = offset + wordOtherAfter.length; |
| } |
| else if (haveWordBefore && !haveWordAfter) { |
| startOffset = offset - wordOtherBefore.length; |
| } |
| else if (haveWordAfter && !haveWordBefore) { |
| endOffset = offset + wordOtherAfter.length; |
| } |
| else if (otherBefore.length <= otherAfter.length) { |
| startOffset = offset - wordOtherBefore.length; |
| } |
| else { |
| endOffset = offset + wordOtherAfter.length; |
| } |
| |
| return new Range(node,startOffset,node,endOffset); |
| } |
| else if (node.nodeType == Node.ELEMENT_NODE) { |
| var nodeBefore = node.childNodes[offset-1]; |
| var nodeAfter = node.childNodes[offset]; |
| |
| if ((nodeBefore != null) && !isWhitespaceTextNode(nodeBefore)) |
| return new Range(node,offset-1,node,offset); |
| else if ((nodeAfter != null) && !isWhitespaceTextNode(nodeAfter)) |
| return new Range(node,offset,node,offset+1); |
| } |
| |
| return null; |
| } |
| |
| // public |
| Selection_selectWordAtCursor = function() |
| { |
| var selRange = Selection_get(); |
| if (selRange == null) |
| return; |
| |
| var pos = Position_closestMatchBackwards(selRange.end,Position_okForMovement); |
| var range = rangeOfWordAtPos(pos); |
| if (range != null) { |
| Selection_set(range.start.node,range.start.offset,range.end.node,range.end.offset); |
| } |
| } |
| |
| // public |
| Selection_dragSelectionBegin = function(x,y,selectWord) |
| { |
| var pos = Position_closestMatchForwards(Position_atPoint(x,y),Position_okForMovement); |
| |
| if (pos == null) { |
| Selection_clear(); |
| return "error"; |
| } |
| |
| Selection_set(pos.node,pos.offset,pos.node,pos.offset); |
| |
| if (selectWord) |
| Selection_selectWordAtCursor(); |
| |
| return "end"; |
| } |
| |
| var selectionHandleEnd = true; |
| |
| function toStartOfWord(pos) |
| { |
| if (Input_isAtWordBoundary(pos,"backward")) |
| return pos; |
| var boundary = Input_toWordBoundary(pos,"backward"); |
| return (boundary != null) ? boundary : pos; |
| } |
| |
| function toEndOfWord(pos) |
| { |
| if (Input_isAtWordBoundary(pos,"forward")) |
| return pos; |
| var boundary = Input_toWordBoundary(pos,"forward"); |
| return (boundary != null) ? boundary : pos; |
| } |
| |
| // public |
| Selection_dragSelectionUpdate = function(x,y,selectWord) |
| { |
| y = Cursor_scrollDocumentForY(y); |
| |
| var pos = Position_closestMatchForwards(Position_atPoint(x,y),Position_okForMovement); |
| var selRange = Selection_get(); |
| if ((pos == null) || (selRange == null)) |
| return "none"; |
| |
| var start = selRange.start; |
| var end = selRange.end; |
| |
| if (selectionHandleEnd) { |
| if (Position_compare(pos,start) < 0) { |
| if (selectWord) |
| pos = toStartOfWord(pos); |
| selectionHandleEnd = false; |
| } |
| else { |
| if (selectWord) |
| pos = toEndOfWord(pos); |
| } |
| Selection_set(start.node,start.offset,pos.node,pos.offset); |
| } |
| else { |
| if (Position_compare(pos,end) > 0) { |
| if (selectWord) |
| pos = toEndOfWord(pos); |
| selectionHandleEnd = true; |
| } |
| else { |
| if (selectWord) |
| pos = toStartOfWord(pos); |
| } |
| Selection_set(pos.node,pos.offset,end.node,end.offset); |
| } |
| |
| return selectionHandleEnd ? "end" : "start"; |
| } |
| |
| function moveBoundary(command) |
| { |
| var range = Selection_get(); |
| if (range == null) |
| return; |
| |
| var pos = null; |
| if (command == "start-left") |
| range.start = pos = Position_prevMatch(range.start,Position_okForMovement); |
| else if (command == "start-right") |
| range.start = pos = Position_nextMatch(range.start,Position_okForMovement); |
| else if (command == "end-left") |
| range.end = pos = Position_prevMatch(range.end,Position_okForMovement); |
| else if (command == "end-right") |
| range.end = pos = Position_nextMatch(range.end,Position_okForMovement); |
| |
| if ((range.start != null) && (range.end != null)) { |
| var result; |
| range = Range_forwards(range); |
| Selection_set(range.start.node,range.start.offset,range.end.node,range.end.offset); |
| if (range.end == pos) |
| return "end"; |
| else if (range.end == pos) |
| return "start"; |
| } |
| return null; |
| } |
| |
| // public |
| Selection_moveStartLeft = function() |
| { |
| return moveBoundary("start-left"); |
| } |
| |
| // public |
| Selection_moveStartRight = function() |
| { |
| return moveBoundary("start-right"); |
| } |
| |
| // public |
| Selection_moveEndLeft = function() |
| { |
| return moveBoundary("end-left"); |
| } |
| |
| // public |
| Selection_moveEndRight = function() |
| { |
| return moveBoundary("end-right"); |
| } |
| |
| // public |
| Selection_setSelectionStartAtCoords = function(x,y) |
| { |
| var position = Position_closestMatchForwards(Position_atPoint(x,y),Position_okForMovement); |
| if (position != null) { |
| position = Position_closestMatchBackwards(position,Position_okForMovement); |
| var selRange = Selection_get(); |
| var newRange = new Range(position.node,position.offset, |
| selRange.end.node,selRange.end.offset); |
| if (Range_isForwards(newRange)) { |
| Selection_set(newRange.start.node,newRange.start.offset, |
| newRange.end.node,newRange.end.offset); |
| } |
| } |
| } |
| |
| // public |
| Selection_setSelectionEndAtCoords = function(x,y) |
| { |
| var position = Position_closestMatchForwards(Position_atPoint(x,y),Position_okForMovement); |
| if (position != null) { |
| position = Position_closestMatchBackwards(position,Position_okForMovement); |
| var selRange = Selection_get(); |
| var newRange = new Range(selRange.start.node,selRange.start.offset, |
| position.node,position.offset); |
| if (Range_isForwards(newRange)) { |
| Selection_set(newRange.start.node,newRange.start.offset, |
| newRange.end.node,newRange.end.offset); |
| } |
| } |
| } |
| |
| // public |
| Selection_setTableSelectionEdgeAtCoords = function(edge,x,y) |
| { |
| if (tableSelection == null) |
| return; |
| |
| var structure = tableSelection.structure; |
| var pointInfo = findCellInTable(structure,x,y); |
| if (pointInfo == null) |
| return; |
| |
| if (edge == "topLeft") { |
| if (pointInfo.row <= tableSelection.bottom) |
| tableSelection.top = pointInfo.row; |
| if (pointInfo.col <= tableSelection.right) |
| tableSelection.left = pointInfo.col; |
| } |
| else if (edge == "bottomRight") { |
| if (pointInfo.row >= tableSelection.top) |
| tableSelection.bottom = pointInfo.row; |
| if (pointInfo.col >= tableSelection.left) |
| tableSelection.right = pointInfo.col; |
| } |
| |
| // FIXME: handle the case where there is no cell at the specified row and column |
| var topLeftCell = Table_get(structure,tableSelection.top,tableSelection.left); |
| var bottomRightCell = Table_get(structure,tableSelection.bottom,tableSelection.right); |
| |
| var topLeftNode = topLeftCell.element.parentNode; |
| var topLeftOffset = DOM_nodeOffset(topLeftCell.element); |
| var bottomRightNode = bottomRightCell.element.parentNode; |
| var bottomRightOffset = DOM_nodeOffset(bottomRightCell.element)+1; |
| |
| Selection_set(topLeftNode,topLeftOffset,bottomRightNode,bottomRightOffset); |
| |
| // FIXME: this could possibly be optimised |
| function findCellInTable(structure,x,y) |
| { |
| for (var r = 0; r < structure.numRows; r++) { |
| for (var c = 0; c < structure.numCols; c++) { |
| var cell = Table_get(structure,r,c); |
| if (cell != null) { |
| var rect = cell.element.getBoundingClientRect(); |
| if ((x >= rect.left) && (x <= rect.right) && |
| (y >= rect.top) && (y <= rect.bottom)) |
| return cell; |
| } |
| } |
| } |
| return null; |
| } |
| } |
| |
| // public |
| Selection_setEmptySelectionAt = function(node,offset) |
| { |
| Selection_set(node,offset,node,offset); |
| } |
| |
| // private |
| function deleteTextSelection(selRange,keepEmpty) |
| { |
| var nodes = Range_getOutermostNodes(selRange); |
| for (var i = 0; i < nodes.length; i++) { |
| var node = nodes[i]; |
| |
| var removeWholeNode = false; |
| |
| if ((node == selRange.start.node) && |
| (node == selRange.end.node)) { |
| var startOffset = selRange.start.offset; |
| var endOffset = selRange.end.offset; |
| if ((node.nodeType == Node.TEXT_NODE) && |
| ((startOffset > 0) || (endOffset < node.nodeValue.length))) { |
| DOM_deleteCharacters(node,startOffset,endOffset); |
| } |
| else { |
| removeWholeNode = true; |
| } |
| } |
| else if (node == selRange.start.node) { |
| var offset = selRange.start.offset; |
| if ((node.nodeType == Node.TEXT_NODE) && (offset > 0)) { |
| DOM_deleteCharacters(node,offset); |
| } |
| else { |
| removeWholeNode = true; |
| } |
| } |
| else if (node == selRange.end.node) { |
| var offset = selRange.end.offset; |
| if ((node.nodeType == Node.TEXT_NODE) && (offset < node.nodeValue.length)) { |
| DOM_deleteCharacters(node,0,offset); |
| } |
| else { |
| removeWholeNode = true; |
| } |
| } |
| else { |
| removeWholeNode = true; |
| } |
| |
| if (removeWholeNode) { |
| switch (node._type) { |
| case HTML_TD: |
| case HTML_TH: |
| DOM_deleteAllChildren(node); |
| break; |
| default: |
| DOM_deleteNode(node); |
| break; |
| } |
| } |
| } |
| |
| var detail = Range_detail(selRange); |
| |
| var sameTextNode = (selRange.start.node == selRange.end.node) && |
| (selRange.start.node.nodeType == Node.TEXT_NODE); |
| |
| if ((detail.startAncestor != null) && (detail.endAncestor != null) && |
| (detail.startAncestor.nextSibling == detail.endAncestor) && |
| !sameTextNode) { |
| prepareForMerge(detail); |
| DOM_mergeWithNextSibling(detail.startAncestor, |
| Formatting_MERGEABLE_BLOCK_AND_INLINE); |
| if (isParagraphNode(detail.startAncestor) && |
| (detail.startAncestor._type != HTML_DIV)) |
| removeParagraphDescendants(detail.startAncestor); |
| } |
| |
| if (!keepEmpty) { |
| var startNode = selRange.start.node; |
| var endNode = selRange.end.node; |
| if (startNode.parentNode != null) |
| delEmpty(selRange,startNode); |
| if (endNode.parentNode != null) |
| delEmpty(selRange,endNode); |
| } |
| |
| Cursor_updateBRAtEndOfParagraph(Range_singleNode(selRange)); |
| } |
| |
| function delEmpty(selRange,node) |
| { |
| while ((node != document.body) && |
| (node.nodeType == Node.ELEMENT_NODE) && |
| (node.firstChild == null)) { |
| |
| if (isTableCell(node) || isTableCell(node.parentNode)) |
| return; |
| |
| if (!fixPositionOutside(selRange.start,node)) |
| break; |
| if (!fixPositionOutside(selRange.end,node)) |
| break; |
| |
| var parent = node.parentNode; |
| Range_trackWhileExecuting(selRange,function() { |
| DOM_deleteNode(node); |
| }); |
| node = parent; |
| } |
| } |
| |
| function fixPositionOutside(pos,node) |
| { |
| if (pos.node == node) { |
| var before = new Position(node.parentNode,DOM_nodeOffset(node)); |
| var after = new Position(node.parentNode,DOM_nodeOffset(node)+1); |
| before = Position_prevMatch(before,Position_okForMovement); |
| after = Position_nextMatch(after,Position_okForMovement); |
| |
| if (before != null) { |
| pos.node = before.node; |
| pos.offset = before.offset; |
| } |
| else if (after != null) { |
| pos.node = after.node; |
| pos.offset = after.offset; |
| } |
| else { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| Selection_deleteRangeContents = function(range,keepEmpty) |
| { |
| Range_trackWhileExecuting(range,function() { |
| DOM_ignoreMutationsWhileExecuting(function() { |
| removeSelectionHighlights(getRangeData(range),true); |
| }); |
| |
| var region = Tables_regionFromRange(range); |
| if (region != null) |
| Tables_deleteRegion(region); |
| else |
| deleteTextSelection(range,keepEmpty); |
| }); |
| |
| Selection_set(range.start.node,range.start.offset,range.start.node,range.start.offset); |
| } |
| |
| Selection_deleteContents = function(keepEmpty) |
| { |
| var range = Selection_get(); |
| if (range == null) |
| return; |
| Selection_deleteRangeContents(range,keepEmpty); |
| } |
| |
| // private |
| function removeParagraphDescendants(parent) |
| { |
| var next; |
| for (var child = parent.firstChild; child != null; child = next) { |
| next = child.nextSibling; |
| removeParagraphDescendants(child); |
| if (isParagraphNode(child)) |
| DOM_removeNodeButKeepChildren(child); |
| } |
| } |
| |
| // private |
| function findFirstParagraph(node) |
| { |
| if (isParagraphNode(node)) |
| return node; |
| if (node._type == HTML_LI) { |
| var nonWhitespaceInline = false; |
| |
| for (var child = node.firstChild; child != null; child = child.nextSibling) { |
| if (isInlineNode(child) && !isWhitespaceTextNode(child)) |
| nonWhitespaceInline = true; |
| |
| if (isParagraphNode(child)) { |
| if (nonWhitespaceInline) |
| return putPrecedingSiblingsInParagraph(node,child); |
| return child; |
| } |
| else if (isListNode(child)) { |
| if (nonWhitespaceInline) |
| return putPrecedingSiblingsInParagraph(node,child); |
| return findFirstParagraph(child); |
| } |
| } |
| if (nonWhitespaceInline) |
| return putPrecedingSiblingsInParagraph(node,null); |
| } |
| return null; |
| |
| function putPrecedingSiblingsInParagraph(parent,node) |
| { |
| var p = DOM_createElement(document,"P"); |
| while (parent.firstChild != node) |
| DOM_appendChild(p,parent.firstChild); |
| return p; |
| } |
| } |
| |
| // private |
| function prepareForMerge(detail) |
| { |
| if (isParagraphNode(detail.startAncestor) && isInlineNode(detail.endAncestor)) { |
| var name = detail.startAncestor.nodeName; // check-ok |
| var newParagraph = DOM_createElement(document,name); |
| DOM_insertBefore(detail.endAncestor.parentNode,newParagraph,detail.endAncestor); |
| DOM_appendChild(newParagraph,detail.endAncestor); |
| detail.endAncestor = newParagraph; |
| } |
| else if (isInlineNode(detail.startAncestor) && isParagraphNode(detail.endAncestor)) { |
| var name = detail.endAncestor.nodeName; // check-ok |
| var newParagraph = DOM_createElement(document,name); |
| DOM_insertBefore(detail.startAncestor.parentNode,newParagraph, |
| detail.startAncestor.nextSibling); |
| DOM_appendChild(newParagraph,detail.startAncestor); |
| detail.startAncestor = newParagraph; |
| } |
| else if (isParagraphNode(detail.startAncestor) && |
| isListNode(detail.endAncestor) && |
| (detail.endAncestor.firstChild._type == HTML_LI)) { |
| var list = detail.endAncestor; |
| var li = detail.endAncestor.firstChild; |
| |
| var paragraph = findFirstParagraph(li); |
| if (paragraph != null) { |
| DOM_insertBefore(list.parentNode,paragraph,list); |
| var name = detail.startAncestor.nodeName; // check-ok |
| DOM_replaceElement(paragraph,name); |
| } |
| if (!nodeHasContent(li)) |
| DOM_deleteNode(li); |
| if (firstChildElement(list) == null) |
| DOM_deleteNode(list); |
| } |
| else if (isParagraphNode(detail.endAncestor) && |
| isListNode(detail.startAncestor) && |
| (detail.startAncestor.lastChild._type == HTML_LI)) { |
| var list = detail.startAncestor; |
| var li = detail.startAncestor.lastChild; |
| var p = detail.endAncestor; |
| var oldLastChild = li.lastChild; |
| while (p.firstChild != null) |
| DOM_insertBefore(li,p.firstChild,null); |
| DOM_deleteNode(p); |
| if (oldLastChild != null) { |
| DOM_mergeWithNextSibling(oldLastChild, |
| Formatting_MERGEABLE_BLOCK_AND_INLINE); |
| } |
| } |
| |
| if ((detail.startAncestor.lastChild != null) && (detail.endAncestor.firstChild != null)) { |
| var childDetail = new Object(); |
| childDetail.startAncestor = detail.startAncestor.lastChild; |
| childDetail.endAncestor = detail.endAncestor.firstChild; |
| prepareForMerge(childDetail); |
| } |
| } |
| |
| // public |
| Selection_clearSelection = function() |
| { |
| Selection_clear(); |
| } |
| |
| // public |
| Selection_preserveWhileExecuting = function(fun) |
| { |
| var range = Selection_get(); |
| |
| // Since the selection may have changed as a result of changes to the document, we |
| // have to call clear() or set() so that undo history is saved |
| if (range == null) { |
| result = fun(); |
| Selection_clear(); |
| } |
| else { |
| result = Range_trackWhileExecuting(range,fun); |
| Selection_set(range.start.node,range.start.offset,range.end.node,range.end.offset); |
| } |
| return result; |
| } |
| |
| Selection_preferElementPositions = function() |
| { |
| var range = Selection_get(); |
| if (range == null) |
| return; |
| range.start = Position_preferElementPosition(range.start); |
| range.end = Position_preferElementPosition(range.end); |
| Selection_set(range.start.node,range.start.offset, |
| range.end.node,range.end.offset); |
| } |
| |
| function getBoundaryContainer(node,topAncestor) |
| { |
| var container = document.body; |
| for (; node != topAncestor.parentNode; node = node.parentNode) { |
| switch (node._type) { |
| case HTML_FIGURE: |
| case HTML_TABLE: |
| container = node; |
| break; |
| } |
| } |
| return container; |
| } |
| |
| function boundaryCompliantRange(range) |
| { |
| if (range == null) |
| return null; |
| |
| var detail = Range_detail(range); |
| var start = range.start; |
| var end = range.end; |
| var startNode = Position_closestActualNode(start); |
| var endNode = Position_closestActualNode(end); |
| var startContainer = getBoundaryContainer(startNode.parentNode,detail.commonAncestor); |
| var endContainer = getBoundaryContainer(endNode.parentNode,detail.commonAncestor); |
| |
| if (startContainer != endContainer) { |
| |
| var doStart = false; |
| var doEnd = false; |
| |
| if (nodeHasAncestor(startContainer,endContainer)) { |
| doStart = true; |
| } |
| else if (nodeHasAncestor(endContainer,startContainer)) { |
| doEnd = true; |
| } |
| else { |
| doStart = true; |
| doEnd = true; |
| } |
| |
| if (doStart && (startContainer != document.body)) |
| start = new Position(startContainer.parentNode,DOM_nodeOffset(startContainer)); |
| if (doEnd && (endContainer != document.body)) |
| end = new Position(endContainer.parentNode,DOM_nodeOffset(endContainer)+1); |
| } |
| return new Range(start.node,start.offset,end.node,end.offset); |
| |
| function nodeHasAncestor(node,ancestor) |
| { |
| for (; node != null; node = node.parentNode) { |
| if (node == ancestor) |
| return true; |
| } |
| return false; |
| } |
| } |
| |
| Selection_print = function() |
| { |
| debug(""); |
| debug(""); |
| debug(""); |
| debug("================================================================================"); |
| |
| var sel = Selection_get(); |
| if (sel == null) { |
| debug("No selection"); |
| return; |
| } |
| |
| printSelectionElement(document.body,""); |
| |
| function printSelectionElement(node,indent) |
| { |
| var className = DOM_getAttribute(node,"class"); |
| if (className != null) |
| debug(indent+node.nodeName+" ("+className+")"); |
| else |
| debug(indent+node.nodeName); |
| |
| var child = node.firstChild; |
| var offset = 0; |
| while (true) { |
| |
| var isStart = ((sel.start.node == node) && (sel.start.offset == offset)); |
| var isEnd = ((sel.end.node == node) && (sel.end.offset == offset)); |
| if (isStart && isEnd) |
| debug(indent+" []"); |
| else if (isStart) |
| debug(indent+" ["); |
| else if (isEnd) |
| debug(indent+" ]"); |
| |
| if (child == null) |
| break; |
| |
| if (child.nodeType == Node.ELEMENT_NODE) |
| printSelectionElement(child,indent+" "); |
| else |
| printSelectionText(child,indent+" "); |
| |
| child = child.nextSibling; |
| offset++; |
| } |
| } |
| |
| function printSelectionText(node,indent) |
| { |
| var value = node.nodeValue; |
| |
| if (sel.end.node == node) { |
| var afterSelection = value.substring(sel.end.offset); |
| value = value.substring(0,sel.end.offset) + "]" + afterSelection; |
| } |
| |
| if (sel.start.node == node) { |
| var beforeSelection = value.substring(0,sel.start.offset); |
| value = beforeSelection + "[" + value.substring(sel.start.offset); |
| } |
| |
| debug(indent+JSON.stringify(value)); |
| } |
| } |
| |
| })(); |