1 /** 2 * Copyright (C) 2012-2013 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, NodeFilter, runtime, core, gui, odf*/ 26 27 /**@typedef{{ 28 convertForwardStepsBetweenFilters:function(number,!core.PositionFilter,!core.PositionFilter):number, 29 convertBackwardStepsBetweenFilters:function(number,!core.PositionFilter,!core.PositionFilter):number, 30 countLinesSteps:function(number,!core.PositionFilter):number, 31 countStepsToLineBoundary:function(number,!core.PositionFilter):number 32 }}*/ 33 gui.StepCounter; 34 35 /** 36 * This class modifies the selection in different ways. 37 * @constructor 38 * @param {core.Cursor} cursor 39 * @param {!Node} rootNode 40 */ 41 gui.SelectionMover = function SelectionMover(cursor, rootNode) { 42 "use strict"; 43 var odfUtils = new odf.OdfUtils(), 44 /**@type{!core.PositionIterator}*/ 45 positionIterator, 46 /**@const*/ 47 FILTER_ACCEPT = core.PositionFilter.FilterResult.FILTER_ACCEPT; 48 49 /** 50 * Resets the positionIterator back to the current cursor position and 51 * returns the iterator. 52 * @return {!core.PositionIterator} 53 */ 54 function getIteratorAtCursor() { 55 // This call relies on setUnfilteredPosition magic. After this call, the 56 // iterator position will be just after the cursor because a position 57 // in the cursor is not allowed. So this only works because the filter 58 // in this instance of PositionIterator disallows positions in the 59 // cursor. 60 positionIterator.setUnfilteredPosition(cursor.getNode(), 0); 61 return positionIterator; 62 } 63 64 /** 65 * Gets the maximum available offset for a given node. For a text node, this 66 * is text length, for element nodes, this will be childNodes.length 67 * @param {!Node} node 68 * @return {!number} 69 */ 70 function getMaximumNodePosition(node) { 71 return node.nodeType === Node.TEXT_NODE ? node.textContent.length : node.childNodes.length; 72 } 73 74 /** 75 * Get the first or last client rectangle based on the useRightEdge flag. 76 * If useRightEdge is set to true, this will return the right-most offset of 77 * the last available rectangle 78 * @param {ClientRectList} clientRectangles 79 * @param {!boolean} useRightEdge 80 * @return {?{top: !number, left: !number, bottom: !number}} 81 */ 82 function getClientRect(clientRectangles, useRightEdge) { 83 var rectangle, 84 simplifiedRectangle = null; 85 86 if (clientRectangles && clientRectangles.length > 0) { 87 rectangle = useRightEdge ? clientRectangles.item(clientRectangles.length - 1) : clientRectangles.item(0); 88 } 89 90 if (rectangle) { 91 simplifiedRectangle = { 92 top: rectangle.top, 93 left: useRightEdge ? rectangle.right : rectangle.left, 94 bottom: rectangle.bottom 95 }; 96 } 97 return simplifiedRectangle; 98 } 99 100 /** 101 * Gets the client rect of a position specified by the container and an 102 * offset. If this is not possible with a range, then the last element's 103 * coordinates are used to guesstimate the position. 104 * @param {!Node} container 105 * @param {!number} offset 106 * @param {!Range} range 107 * @param {boolean=} useRightEdge Default value is false. Used when 108 * searching for the closest visually 109 * equivalent rectangle, starting at the 110 * specified container offset. In these 111 * circumstances, the right-side of the last 112 * client rectangle actually defines the 113 * visual position. 114 * @return {{top: !number, left: !number, bottom: !number}} 115 */ 116 function getVisibleRect(container, offset, range, useRightEdge) { 117 var rectangle, 118 nodeType = container.nodeType; 119 120 // There are various places where the list of client rects will be empty: 121 // - Empty text nodes 122 // - Non-visible elements (e.g., collapsed or wrapped whitespace, hidden elements etc.) 123 // - Non-visible coordinates (e.g., the last position of a paragraph is a selection position, but not a rendered character) 124 // In each case, we need to find the visually equivalent rectangle just preceding this container+offset 125 126 // Step 1 - Select just the collapsed point 127 range.setStart(container, offset); 128 range.collapse(!useRightEdge); 129 rectangle = getClientRect(range.getClientRects(), useRightEdge === true); 130 131 if (!rectangle && offset > 0) { 132 // Fallback 1 - Select the offset & preceding if available 133 range.setStart(container, offset - 1); 134 range.setEnd(container, offset); 135 rectangle = getClientRect(range.getClientRects(), true); 136 } 137 138 if (!rectangle) { 139 if (nodeType === Node.ELEMENT_NODE && offset > 0 140 && /**@type{!Element}*/(container).childNodes.length >= offset) { 141 // Fallback 2 - there are other child nodes directly preceding this offset. Try those 142 rectangle = getVisibleRect(container, offset - 1, range, true); 143 } else if (container.nodeType === Node.TEXT_NODE && offset > 0) { 144 // Fallback 3 - this is a text node, so is either in an invisible container, or is collapsed whitespace, see if an adjacent character is visible 145 rectangle = getVisibleRect(container, offset - 1, range, true); 146 } else if (container.previousSibling) { 147 // Fallback 4 - Has a previous sibling, try using that 148 rectangle = getVisibleRect(container.previousSibling, getMaximumNodePosition(container.previousSibling), range, true); 149 } else if (container.parentNode && container.parentNode !== rootNode) { 150 // Fallback 5 - try using the parent container. Had no previous siblings, so look for the first offset 151 rectangle = getVisibleRect(container.parentNode, 0, range, false); 152 } else { 153 // Fallback 6 - no previous siblings have been found, try and return the root node's bounding container 154 // Assert container === rootNode 155 range.selectNode(rootNode); 156 rectangle = getClientRect(range.getClientRects(), false); 157 } 158 } 159 runtime.assert(Boolean(rectangle), "No visible rectangle found"); 160 return /**@type{{top: !number, left: !number, bottom: !number}}*/(rectangle); 161 } 162 163 /** 164 * Returns the number of positions to the right the (steps, filter1) pair 165 * is equivalent to in filter2 space. 166 * @param {!number} stepsFilter1 Number of filter1 steps to count 167 * @param {!core.PositionFilter} filter1 168 * @param {!core.PositionFilter} filter2 169 * @return {!number} Equivalent steps in filter2 space 170 */ 171 function convertForwardStepsBetweenFilters(stepsFilter1, filter1, filter2) { 172 var iterator = getIteratorAtCursor(), 173 watch = new core.LoopWatchDog(10000), 174 pendingStepsFilter2 = 0, 175 stepsFilter2 = 0; 176 while (stepsFilter1 > 0 && iterator.nextPosition()) { 177 watch.check(); 178 if (filter2.acceptPosition(iterator) === FILTER_ACCEPT) { 179 pendingStepsFilter2 += 1; 180 if (filter1.acceptPosition(iterator) === FILTER_ACCEPT) { 181 stepsFilter2 += pendingStepsFilter2; 182 pendingStepsFilter2 = 0; 183 stepsFilter1 -= 1; 184 } 185 } 186 } 187 return stepsFilter2; 188 } 189 /** 190 * Returns the number of positions to the left the (steps, filter1) pair 191 * is equivalent to in filter2 space. 192 * @param {!number} stepsFilter1 Number of filter1 steps to count 193 * @param {!core.PositionFilter} filter1 194 * @param {!core.PositionFilter} filter2 195 * @return {!number} Equivalent steps in filter2 space 196 */ 197 function convertBackwardStepsBetweenFilters(stepsFilter1, filter1, filter2) { 198 var iterator = getIteratorAtCursor(), 199 watch = new core.LoopWatchDog(10000), 200 pendingStepsFilter2 = 0, 201 stepsFilter2 = 0; 202 while (stepsFilter1 > 0 && iterator.previousPosition()) { 203 watch.check(); 204 if (filter2.acceptPosition(iterator) === FILTER_ACCEPT) { 205 pendingStepsFilter2 += 1; 206 if (filter1.acceptPosition(iterator) === FILTER_ACCEPT) { 207 stepsFilter2 += pendingStepsFilter2; 208 pendingStepsFilter2 = 0; 209 stepsFilter1 -= 1; 210 } 211 } 212 } 213 return stepsFilter2; 214 } 215 216 /** 217 * Return the number of steps needed to move across one line in the specified direction. 218 * If it is not possible to move across one line, then 0 is returned. 219 * 220 * @param {!number} direction -1 for upwards, +1 for downwards 221 * @param {!core.PositionFilter} filter 222 * @param {!core.PositionIterator} iterator 223 * @return {!number} steps 224 */ 225 function countLineSteps(filter, direction, iterator) { 226 var c = iterator.container(), 227 steps = 0, 228 bestContainer = null, 229 bestOffset, 230 bestXDiff = 10, 231 xDiff, 232 bestCount = 0, 233 top, 234 left, 235 lastTop, 236 rect, 237 range = /**@type{!Range}*/(rootNode.ownerDocument.createRange()), 238 watch = new core.LoopWatchDog(10000); 239 240 // Get the starting position 241 rect = getVisibleRect(c, iterator.unfilteredDomOffset(), range); 242 243 top = rect.top; 244 left = rect.left; 245 lastTop = top; 246 247 while ((direction < 0 ? iterator.previousPosition() : iterator.nextPosition()) === true) { 248 watch.check(); 249 if (filter.acceptPosition(iterator) === FILTER_ACCEPT) { 250 steps += 1; 251 252 c = iterator.container(); 253 rect = getVisibleRect(c, iterator.unfilteredDomOffset(), range); 254 255 if (rect.top !== top) { // Not on the initial line anymore 256 if (rect.top !== lastTop && lastTop !== top) { // Not even on the next line 257 break; 258 } 259 lastTop = rect.top; 260 xDiff = Math.abs(left - rect.left); 261 if (bestContainer === null || xDiff < bestXDiff) { 262 bestContainer = c; 263 bestOffset = iterator.unfilteredDomOffset(); 264 bestXDiff = xDiff; 265 bestCount = steps; 266 } 267 } 268 } 269 } 270 271 if (bestContainer !== null) { 272 iterator.setUnfilteredPosition(bestContainer, /**@type {!number}*/(bestOffset)); 273 steps = bestCount; 274 } else { 275 steps = 0; 276 } 277 278 range.detach(); 279 return steps; 280 } 281 /** 282 * @param {!number} lines negative number for upwards, positive number for downwards 283 * @param {!core.PositionFilter} filter 284 * @return {!number} steps 285 */ 286 function countLinesSteps(lines, filter) { 287 var iterator = getIteratorAtCursor(), 288 stepCount = 0, 289 steps = 0, 290 direction = lines < 0 ? -1 : 1; 291 292 lines = Math.abs(lines); 293 // move back in the document, until a position is found for which the 294 // top is smaller than initially and the left is closest 295 while (lines > 0) { 296 stepCount += countLineSteps(filter, direction, iterator); 297 if (stepCount === 0) { 298 break; 299 } 300 steps += stepCount; 301 lines -= 1; 302 } 303 return steps * direction; 304 } 305 /** 306 * Returns the number of steps needed to move to the beginning/end of the 307 * line. 308 * @param {!number} direction -1 for beginning of the line, 1 for end of the 309 * line 310 * @param {!core.PositionFilter} filter 311 * @return {!number} steps 312 */ 313 function countStepsToLineBoundary(direction, filter) { 314 var fnNextPos, increment, 315 lastRect, rect, onSameLine, 316 iterator = getIteratorAtCursor(), 317 paragraphNode = odfUtils.getParagraphElement(iterator.getCurrentNode()), 318 steps = 0, 319 range = /**@type{!Range}*/(rootNode.ownerDocument.createRange()); 320 321 if (direction < 0) { 322 fnNextPos = iterator.previousPosition; 323 increment = -1; 324 } else { 325 fnNextPos = iterator.nextPosition; 326 increment = 1; 327 } 328 329 lastRect = getVisibleRect(iterator.container(), iterator.unfilteredDomOffset(), range); 330 while (fnNextPos.call(iterator)) { 331 if (filter.acceptPosition(iterator) === FILTER_ACCEPT) { 332 // hit another paragraph node, so won't be the same line 333 if (odfUtils.getParagraphElement(iterator.getCurrentNode()) !== paragraphNode) { 334 break; 335 } 336 337 rect = getVisibleRect(iterator.container(), iterator.unfilteredDomOffset(), range); 338 if (rect.bottom !== lastRect.bottom) { // most cases it means hit the line above/below 339 // if top and bottom overlaps, assume they are on the same line 340 onSameLine = (rect.top >= lastRect.top && rect.bottom < lastRect.bottom) 341 || (rect.top <= lastRect.top && rect.bottom > lastRect.bottom); 342 if (!onSameLine) { 343 break; 344 } 345 } 346 347 steps += increment; 348 lastRect = rect; 349 } 350 } 351 352 range.detach(); 353 return steps; 354 } 355 356 /** 357 * @return {!gui.StepCounter} 358 */ 359 this.getStepCounter = function () { 360 return { 361 convertForwardStepsBetweenFilters: convertForwardStepsBetweenFilters, 362 convertBackwardStepsBetweenFilters: convertBackwardStepsBetweenFilters, 363 countLinesSteps: countLinesSteps, 364 countStepsToLineBoundary: countStepsToLineBoundary 365 }; 366 }; 367 function init() { 368 positionIterator = gui.SelectionMover.createPositionIterator(rootNode); 369 var range = rootNode.ownerDocument.createRange(); 370 range.setStart(positionIterator.container(), positionIterator.unfilteredDomOffset()); 371 range.collapse(true); 372 cursor.setSelectedRange(range); 373 } 374 init(); 375 }; 376 /** 377 * @param {!Node} rootNode 378 * @return {!core.PositionIterator} 379 */ 380 gui.SelectionMover.createPositionIterator = function (rootNode) { 381 "use strict"; 382 /** 383 * @constructor 384 * @extends NodeFilter 385 */ 386 function CursorFilter() { 387 /** 388 * @param {?Node} node 389 * @return {!number} 390 */ 391 this.acceptNode = function (node) { 392 if (!node || node.namespaceURI === "urn:webodf:names:cursor" || 393 node.namespaceURI === "urn:webodf:names:editinfo") { 394 return NodeFilter.FILTER_REJECT; 395 } 396 return NodeFilter.FILTER_ACCEPT; 397 }; 398 } 399 var filter = new CursorFilter(); 400 return new core.PositionIterator(rootNode, 5, filter, false); 401 }; 402