1 /** 2 * Copyright (C) 2012 KO GmbH <copyright@kogmbh.com> 3 * 4 * @licstart 5 * This file is part of WebODF. 6 * 7 * WebODF is free software: you can redistribute it and/or modify it 8 * under the terms of the GNU Affero General Public License (GNU AGPL) 9 * as published by the Free Software Foundation, either version 3 of 10 * the License, or (at your option) any later version. 11 * 12 * WebODF is distributed in the hope that it will be useful, but 13 * WITHOUT ANY WARRANTY; without even the implied warranty of 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 * GNU Affero General Public License for more details. 16 * 17 * You should have received a copy of the GNU Affero General Public License 18 * along with WebODF. If not, see <http://www.gnu.org/licenses/>. 19 * @licend 20 * 21 * @source: http://www.webodf.org/ 22 * @source: https://github.com/kogmbh/WebODF/ 23 */ 24 25 /*global Node, core, ops, runtime*/ 26 27 /** 28 * @class 29 * A cursor is a dom node that visually represents a cursor in a DOM tree. 30 * It should stay synchronized with the selection in the document. When 31 * there is only one collapsed selection range, a cursor should be shown at 32 * that point. 33 * 34 * Putting the cursor in the DOM tree modifies the DOM, so care should be taken 35 * to keep the selection consistent. If e.g. a selection is drawn over the 36 * cursor, and the cursor is updated to the selection, the cursor is removed 37 * from the DOM because the selection is not collapsed. This means that the 38 * offsets of the selection may have to be changed. 39 * 40 * When the selection is collapsed, the cursor is placed after the point of the 41 * selection and the selection will stay valid. However, if the cursor was 42 * placed in the DOM tree and was counted in the offset, the offset in the 43 * selection should be decreased by one. 44 * 45 * Even when the selection allows for a cursor, it might be desireable to hide 46 * the cursor by not letting it be part of the DOM. 47 * 48 * @constructor 49 * @param {!Document} document The DOM document in which the cursor is placed 50 * @param {!string} memberId The memberid this cursor is assigned to 51 */ 52 core.Cursor = function Cursor(document, memberId) { 53 "use strict"; 54 var cursorns = 'urn:webodf:names:cursor', 55 /**@type{!Element}*/ 56 cursorNode = document.createElementNS(cursorns, 'cursor'), 57 /**@type{!Element}*/ 58 anchorNode = document.createElementNS(cursorns, 'anchor'), 59 forwardSelection, 60 recentlyModifiedNodes = [], 61 /**@type{!Range}*/ 62 selectedRange = /**@type{!Range}*/(document.createRange()), 63 isCollapsed, 64 domUtils = core.DomUtils; 65 66 /** 67 * Split a text node and put the cursor into it. 68 * @param {!Node} node 69 * @param {!Text} container 70 * @param {!number} offset 71 * @return {undefined} 72 */ 73 function putIntoTextNode(node, container, offset) { 74 runtime.assert(Boolean(container), "putCursorIntoTextNode: invalid container"); 75 var parent = container.parentNode; 76 runtime.assert(Boolean(parent), "putCursorIntoTextNode: container without parent"); 77 runtime.assert(offset >= 0 && offset <= container.length, "putCursorIntoTextNode: offset is out of bounds"); 78 79 if (offset === 0) { 80 parent.insertBefore(node, container); 81 } else if (offset === container.length) { 82 parent.insertBefore(node, container.nextSibling); 83 } else { 84 container.splitText(offset); 85 parent.insertBefore(node, container.nextSibling); 86 } 87 } 88 /** 89 * Remove the cursor from the tree. 90 * @param {!Element} node 91 */ 92 function removeNode(node) { 93 if (node.parentNode) { 94 recentlyModifiedNodes.push(node.previousSibling); 95 recentlyModifiedNodes.push(node.nextSibling); 96 node.parentNode.removeChild(node); 97 } 98 } 99 100 /** 101 * Put the cursor at a particular position. 102 * @param {!Node} node 103 * @param {!Node} container 104 * @param {!number} offset 105 * @return {undefined} 106 */ 107 function putNode(node, container, offset) { 108 if (container.nodeType === Node.TEXT_NODE) { 109 putIntoTextNode(node, /**@type{!Text}*/(container), offset); 110 } else if (container.nodeType === Node.ELEMENT_NODE) { 111 container.insertBefore(node, container.childNodes.item(offset)); 112 } 113 recentlyModifiedNodes.push(node.previousSibling); 114 recentlyModifiedNodes.push(node.nextSibling); 115 } 116 117 /** 118 * Gets the earliest selection node in the document 119 * @return {!Node} 120 */ 121 function getStartNode() { 122 return forwardSelection ? anchorNode : cursorNode; 123 } 124 125 /** 126 * Gets the latest selection node in the document 127 * @return {!Node} 128 */ 129 function getEndNode() { 130 return forwardSelection ? cursorNode : anchorNode; 131 } 132 /** 133 * Obtain the node representing the cursor. This is 134 * the selection end point 135 * @return {!Element} 136 */ 137 this.getNode = function () { 138 return cursorNode; 139 }; 140 /** 141 * Obtain the node representing the selection start point. 142 * If a 0-length range is selected (e.g., by clicking without 143 * dragging),, this will return the exact same node as getNode 144 * @return {!Element} 145 */ 146 this.getAnchorNode = function () { 147 return anchorNode.parentNode ? anchorNode : cursorNode; 148 }; 149 /** 150 * Obtain the selection to which the cursor corresponds. 151 * @return {!Range} 152 */ 153 this.getSelectedRange = function () { 154 if (isCollapsed) { 155 selectedRange.setStartBefore(cursorNode); 156 selectedRange.collapse(true); 157 } else { 158 selectedRange.setStartAfter(getStartNode()); 159 selectedRange.setEndBefore(getEndNode()); 160 } 161 return selectedRange; 162 }; 163 /** 164 * Synchronize the cursor to a specific range 165 * If there is a single collapsed selection range, the cursor will be placed 166 * there. If not, the cursor will be removed from the document tree. 167 * @param {!Range} range 168 * @param {boolean=} isForwardSelection Set to true to indicate the direction of the 169 * range is startContainer => endContainer. This should be false if 170 * the user creates a selection that ends before it starts in the document (i.e., 171 * drags the range backwards from the start point) 172 * @return {undefined} 173 */ 174 this.setSelectedRange = function (range, isForwardSelection) { 175 if (selectedRange && selectedRange !== range) { 176 selectedRange.detach(); 177 } 178 selectedRange = range; 179 forwardSelection = isForwardSelection !== false; 180 isCollapsed = range.collapsed; 181 182 // TODO the nodes need to be added and removed in the right order to preserve the range 183 if (range.collapsed) { 184 removeNode(anchorNode); 185 removeNode(cursorNode); 186 putNode(cursorNode, /**@type {!Node}*/(range.startContainer), range.startOffset); 187 } else { 188 removeNode(anchorNode); 189 removeNode(cursorNode); 190 // putting in the end node first eliminates the chance the position of the start node is destroyed 191 putNode(getEndNode(), /**@type {!Node}*/(range.endContainer), range.endOffset); 192 putNode(getStartNode(), /**@type {!Node}*/(range.startContainer), range.startOffset); 193 } 194 recentlyModifiedNodes.forEach(domUtils.normalizeTextNodes); 195 recentlyModifiedNodes.length = 0; 196 }; 197 /** 198 * Returns if the selection of this cursor has the 199 * same direction as the direction of the range 200 * @return {boolean} 201 */ 202 this.hasForwardSelection = function () { 203 return forwardSelection; 204 }; 205 /** 206 * Remove the cursor from the document tree. 207 * @return {undefined} 208 */ 209 this.remove = function () { 210 removeNode(cursorNode); 211 recentlyModifiedNodes.forEach(domUtils.normalizeTextNodes); 212 recentlyModifiedNodes.length = 0; 213 }; 214 215 function init() { 216 // mark cursornode with memberid 217 cursorNode.setAttributeNS(cursorns, "memberId", memberId); 218 anchorNode.setAttributeNS(cursorns, "memberId", memberId); 219 } 220 221 init(); 222 }; 223