1 /** 2 * Copyright (C) 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 runtime, core, gui, odf, ops, Node */ 26 27 /** 28 * @constructor 29 * @implements {core.Destroyable} 30 * @param {!ops.Session} session 31 * @param {!string} inputMemberId 32 */ 33 gui.SelectionController = function SelectionController(session, inputMemberId) { 34 "use strict"; 35 var odtDocument = session.getOdtDocument(), 36 domUtils = core.DomUtils, 37 odfUtils = odf.OdfUtils, 38 baseFilter = odtDocument.getPositionFilter(), 39 guiStepUtils = new gui.GuiStepUtils(), 40 rootFilter = odtDocument.createRootFilter(inputMemberId), 41 /**@type{?function():(!number|undefined)}*/ 42 caretXPositionLocator = null, 43 /**@type{!number|undefined}*/ 44 lastXPosition, 45 /**@type{!core.ScheduledTask}*/ 46 resetLastXPositionTask, 47 TRAILING_SPACE = odf.WordBoundaryFilter.IncludeWhitespace.TRAILING, 48 LEADING_SPACE = odf.WordBoundaryFilter.IncludeWhitespace.LEADING, 49 PREVIOUS = core.StepDirection.PREVIOUS, 50 NEXT = core.StepDirection.NEXT, 51 // Number of milliseconds to keep the user's last up/down caret X position for 52 /**@const*/ UPDOWN_NAVIGATION_RESET_DELAY_MS = 2000; 53 54 /** 55 * @param {!ops.Operation} op 56 * @return undefined; 57 */ 58 function resetLastXPosition(op) { 59 var opspec = op.spec(); 60 if (op.isEdit || opspec.memberid === inputMemberId) { 61 lastXPosition = undefined; 62 resetLastXPositionTask.cancel(); 63 } 64 } 65 66 /** 67 * Create a new step iterator with the base Odt filter, and a root filter for the current input member. 68 * The step iterator subtree is set to the root of the current cursor node 69 * @return {!core.StepIterator} 70 */ 71 function createKeyboardStepIterator() { 72 var cursor = odtDocument.getCursor(inputMemberId), 73 node = cursor.getNode(); 74 75 return odtDocument.createStepIterator(node, 0, [baseFilter, rootFilter], odtDocument.getRootElement(node)); 76 } 77 78 /** 79 * Create a new step iterator that will iterate by word boundaries 80 * @param {!Node} node 81 * @param {!number} offset 82 * @param {!odf.WordBoundaryFilter.IncludeWhitespace} includeWhitespace 83 * @return {!core.StepIterator} 84 */ 85 function createWordBoundaryStepIterator(node, offset, includeWhitespace) { 86 var wordBoundaryFilter = new odf.WordBoundaryFilter(odtDocument, includeWhitespace), 87 nodeRoot = odtDocument.getRootElement(node) || odtDocument.getRootNode(), 88 nodeRootFilter = odtDocument.createRootFilter(nodeRoot); 89 return odtDocument.createStepIterator(node, offset, [baseFilter, nodeRootFilter, wordBoundaryFilter], nodeRoot); 90 } 91 92 /** 93 * Derive a selection-type object from the provided cursor 94 * @param {!{anchorNode: Node, anchorOffset: !number, focusNode: Node, focusOffset: !number}} selection 95 * @return {{range: !Range, hasForwardSelection: !boolean}} 96 */ 97 function selectionToRange(selection) { 98 var hasForwardSelection = domUtils.comparePoints(/**@type{!Node}*/(selection.anchorNode), selection.anchorOffset, 99 /**@type{!Node}*/(selection.focusNode), selection.focusOffset) >= 0, 100 range = selection.focusNode.ownerDocument.createRange(); 101 if (hasForwardSelection) { 102 range.setStart(selection.anchorNode, selection.anchorOffset); 103 range.setEnd(selection.focusNode, selection.focusOffset); 104 } else { 105 range.setStart(selection.focusNode, selection.focusOffset); 106 range.setEnd(selection.anchorNode, selection.anchorOffset); 107 } 108 return { 109 range: range, 110 hasForwardSelection: hasForwardSelection 111 }; 112 } 113 this.selectionToRange = selectionToRange; 114 115 /** 116 * Derive a selection-type object from the provided cursor 117 * @param {!Range} range 118 * @param {!boolean} hasForwardSelection 119 * @return {!{anchorNode: !Node, anchorOffset: !number, focusNode: !Node, focusOffset: !number}} 120 */ 121 function rangeToSelection(range, hasForwardSelection) { 122 if (hasForwardSelection) { 123 return { 124 anchorNode: /**@type{!Node}*/(range.startContainer), 125 anchorOffset: range.startOffset, 126 focusNode: /**@type{!Node}*/(range.endContainer), 127 focusOffset: range.endOffset 128 }; 129 } 130 return { 131 anchorNode: /**@type{!Node}*/(range.endContainer), 132 anchorOffset: range.endOffset, 133 focusNode: /**@type{!Node}*/(range.startContainer), 134 focusOffset: range.startOffset 135 }; 136 } 137 this.rangeToSelection = rangeToSelection; 138 139 /** 140 * @param {!number} position 141 * @param {!number} length 142 * @param {string=} selectionType 143 * @return {!ops.Operation} 144 */ 145 function createOpMoveCursor(position, length, selectionType) { 146 var op = new ops.OpMoveCursor(); 147 op.init({ 148 memberid: inputMemberId, 149 position: position, 150 length: length || 0, 151 selectionType: selectionType 152 }); 153 return op; 154 } 155 156 /** 157 * Move or extend the local member's selection to the specified focus point. 158 * 159 * @param {!Node} focusNode 160 * @param {!number} focusOffset 161 * @param {!boolean} extend Set to true to extend the selection (i.e., the current selection anchor 162 * will remain unchanged) 163 * @return {undefined} 164 */ 165 function moveCursorFocusPoint(focusNode, focusOffset, extend) { 166 var cursor, 167 newSelection, 168 newCursorSelection; 169 170 cursor = odtDocument.getCursor(inputMemberId); 171 newSelection = rangeToSelection(cursor.getSelectedRange(), cursor.hasForwardSelection()); 172 newSelection.focusNode = focusNode; 173 newSelection.focusOffset = focusOffset; 174 175 if (!extend) { 176 newSelection.anchorNode = newSelection.focusNode; 177 newSelection.anchorOffset = newSelection.focusOffset; 178 } 179 newCursorSelection = odtDocument.convertDomToCursorRange(newSelection); 180 session.enqueue([createOpMoveCursor(newCursorSelection.position, newCursorSelection.length)]); 181 } 182 183 /** 184 * @param {!Node} frameNode 185 */ 186 function selectImage(frameNode) { 187 var frameRoot = odtDocument.getRootElement(frameNode), 188 frameRootFilter = odtDocument.createRootFilter(frameRoot), 189 stepIterator = odtDocument.createStepIterator(frameNode, 0, [frameRootFilter, odtDocument.getPositionFilter()], frameRoot), 190 anchorNode, 191 anchorOffset, 192 newSelection, 193 op; 194 195 if (!stepIterator.roundToPreviousStep()) { 196 runtime.assert(false, "No walkable position before frame"); 197 } 198 anchorNode = stepIterator.container(); 199 anchorOffset = stepIterator.offset(); 200 201 stepIterator.setPosition(frameNode, frameNode.childNodes.length); 202 if (!stepIterator.roundToNextStep()) { 203 runtime.assert(false, "No walkable position after frame"); 204 } 205 206 newSelection = odtDocument.convertDomToCursorRange({ 207 anchorNode: anchorNode, 208 anchorOffset: anchorOffset, 209 focusNode: stepIterator.container(), 210 focusOffset: stepIterator.offset() 211 }); 212 op = createOpMoveCursor(newSelection.position, newSelection.length, ops.OdtCursor.RegionSelection); 213 session.enqueue([op]); 214 } 215 this.selectImage = selectImage; 216 217 /** 218 * Expands the supplied selection to the nearest word boundaries 219 * @param {!Range} range 220 */ 221 function expandToWordBoundaries(range) { 222 var stepIterator; 223 224 stepIterator = createWordBoundaryStepIterator(/**@type{!Node}*/(range.startContainer), range.startOffset, TRAILING_SPACE); 225 if (stepIterator.roundToPreviousStep()) { 226 range.setStart(stepIterator.container(), stepIterator.offset()); 227 } 228 229 stepIterator = createWordBoundaryStepIterator(/**@type{!Node}*/(range.endContainer), range.endOffset, LEADING_SPACE); 230 if (stepIterator.roundToNextStep()) { 231 range.setEnd(stepIterator.container(), stepIterator.offset()); 232 } 233 } 234 this.expandToWordBoundaries = expandToWordBoundaries; 235 236 /** 237 * Expands the supplied selection to the nearest paragraph boundaries 238 * @param {!Range} range 239 */ 240 function expandToParagraphBoundaries(range) { 241 var paragraphs = odfUtils.getParagraphElements(range), 242 startParagraph = paragraphs[0], 243 endParagraph = paragraphs[paragraphs.length - 1]; 244 245 if (startParagraph) { 246 range.setStart(startParagraph, 0); 247 } 248 249 if (endParagraph) { 250 if (odfUtils.isParagraph(range.endContainer) && range.endOffset === 0) { 251 // Chrome's built-in paragraph expansion will put the end of the selection 252 // at (p,0) of the FOLLOWING paragraph. Round this back down to ensure 253 // the next paragraph doesn't get incorrectly selected 254 range.setEndBefore(endParagraph); 255 } else { 256 range.setEnd(endParagraph, endParagraph.childNodes.length); 257 } 258 } 259 } 260 this.expandToParagraphBoundaries = expandToParagraphBoundaries; 261 262 /** 263 * Rounds to the closest available step inside the supplied root, and preferably 264 * inside the original paragraph the node and offset are within. If (node, offset) is 265 * outside the root, the closest root boundary is used instead. 266 * This function will assert if no valid step is found within the supplied root. 267 * 268 * @param {!Node} root Root to contain iteration within 269 * @param {!Array.<!core.PositionFilter>} filters Position filters 270 * @param {!Range} range Range to modify 271 * @param {!boolean} modifyStart Set to true to modify the start container & offset. If false, the end 272 * container and offset will be modified instead. 273 * 274 * @return {undefined} 275 */ 276 function roundToClosestStep(root, filters, range, modifyStart) { 277 var stepIterator, 278 node, 279 offset; 280 281 if (modifyStart) { 282 node = /**@type{!Node}*/(range.startContainer); 283 offset = range.startOffset; 284 } else { 285 node = /**@type{!Node}*/(range.endContainer); 286 offset = range.endOffset; 287 } 288 289 if (!domUtils.containsNode(root, node)) { 290 if (domUtils.comparePoints(root, 0, node, offset) < 0) { 291 offset = 0; 292 } else { 293 offset = root.childNodes.length; 294 } 295 node = root; 296 } 297 stepIterator = odtDocument.createStepIterator(node, offset, filters, odfUtils.getParagraphElement(node) || root); 298 if (!stepIterator.roundToClosestStep()) { 299 runtime.assert(false, "No step found in requested range"); 300 } 301 if (modifyStart) { 302 range.setStart(stepIterator.container(), stepIterator.offset()); 303 } else { 304 range.setEnd(stepIterator.container(), stepIterator.offset()); 305 } 306 } 307 308 /** 309 * Set the user's cursor to the specified selection. If the start and end containers are in different roots, 310 * the anchor's root constraint is used (the anchor is the startContainer for a forward selection, or the 311 * endContainer for a reverse selection). 312 * 313 * If both the range start and range end are outside of the canvas element, no operations are generated. 314 * 315 * @param {!Range} range 316 * @param {!boolean} hasForwardSelection Set to true to indicate the range is from anchor (startContainer) to focus 317 * (endContainer) 318 * @param {number=} clickCount A value of 2 denotes expandToWordBoundaries, while a value of 3 and above will expand 319 * to paragraph boundaries. 320 * @return {undefined} 321 */ 322 function selectRange(range, hasForwardSelection, clickCount) { 323 var canvasElement = odtDocument.getOdfCanvas().getElement(), 324 validSelection, 325 startInsideCanvas, 326 endInsideCanvas, 327 existingSelection, 328 newSelection, 329 anchorRoot, 330 filters = [baseFilter], 331 op; 332 333 startInsideCanvas = domUtils.containsNode(canvasElement, range.startContainer); 334 endInsideCanvas = domUtils.containsNode(canvasElement, range.endContainer); 335 if (!startInsideCanvas && !endInsideCanvas) { 336 return; 337 } 338 339 if (startInsideCanvas && endInsideCanvas) { 340 // Expansion behaviour should only occur when double & triple clicking is inside the canvas 341 if (clickCount === 2) { 342 expandToWordBoundaries(range); 343 } else if (clickCount >= 3) { 344 expandToParagraphBoundaries(range); 345 } 346 } 347 348 if (hasForwardSelection) { 349 anchorRoot = odtDocument.getRootElement(/**@type{!Node}*/(range.startContainer)); 350 } else { 351 anchorRoot = odtDocument.getRootElement(/**@type{!Node}*/(range.endContainer)); 352 } 353 if (!anchorRoot) { 354 // If the range end is not within a root element, use the document root instead 355 anchorRoot = odtDocument.getRootNode(); 356 } 357 filters.push(odtDocument.createRootFilter(anchorRoot)); 358 roundToClosestStep(anchorRoot, filters, range, true); 359 roundToClosestStep(anchorRoot, filters, range, false); 360 validSelection = rangeToSelection(range, hasForwardSelection); 361 newSelection = odtDocument.convertDomToCursorRange(validSelection); 362 existingSelection = odtDocument.getCursorSelection(inputMemberId); 363 if (newSelection.position !== existingSelection.position || newSelection.length !== existingSelection.length) { 364 op = createOpMoveCursor(newSelection.position, newSelection.length, ops.OdtCursor.RangeSelection); 365 session.enqueue([op]); 366 } 367 } 368 this.selectRange = selectRange; 369 370 /** 371 * @param {!core.StepDirection} direction 372 * @param {!boolean} extend 373 * @return {undefined} 374 */ 375 function moveCursor(direction, extend) { 376 var stepIterator = createKeyboardStepIterator(); 377 378 if (stepIterator.advanceStep(direction)) { 379 moveCursorFocusPoint(stepIterator.container(), stepIterator.offset(), extend); 380 } 381 } 382 383 /** 384 * @return {!boolean} 385 */ 386 function moveCursorToLeft() { 387 moveCursor(PREVIOUS, false); 388 return true; 389 } 390 this.moveCursorToLeft = moveCursorToLeft; 391 392 /** 393 * @return {!boolean} 394 */ 395 function moveCursorToRight() { 396 moveCursor(NEXT, false); 397 return true; 398 } 399 this.moveCursorToRight = moveCursorToRight; 400 401 /** 402 * @return {!boolean} 403 */ 404 function extendSelectionToLeft() { 405 moveCursor(PREVIOUS, true); 406 return true; 407 } 408 this.extendSelectionToLeft = extendSelectionToLeft; 409 410 /** 411 * @return {!boolean} 412 */ 413 function extendSelectionToRight() { 414 moveCursor(NEXT, true); 415 return true; 416 } 417 this.extendSelectionToRight = extendSelectionToRight; 418 419 /** 420 * Sets the position locator function for the local input member's visual caret. If 421 * set to null, cursor movement by line will be disabled. 422 * 423 * @param {?function():(!number|undefined)} locator 424 * @return {undefined} 425 */ 426 this.setCaretXPositionLocator = function(locator) { 427 caretXPositionLocator = locator; 428 }; 429 430 /** 431 * @param {!core.StepDirection} direction PREVIOUS for upwards NEXT for downwards 432 * @param {!boolean} extend 433 * @return {undefined} 434 */ 435 function moveCursorByLine(direction, extend) { 436 var stepIterator, 437 currentX = lastXPosition, 438 stepScanners = [new gui.LineBoundaryScanner(), new gui.ParagraphBoundaryScanner()]; 439 440 // Both a line boundary AND a paragraph boundary scanner are necessary to ensure the caret stops correctly 441 // inside an empty paragraph. 442 // The line boundary scanner requires a visible client rect in order to detect a line break, but for an 443 // empty paragraph, there is no visible leading or trailing rect as there aren't any visible children. 444 // As a result, the line boundary detection can't determine if an empty paragraph is a line-wrap point, but 445 // the paragraph boundary scanner *will* correctly determine that step iterator has moved beyond the 446 // current paragraph. 447 448 if (currentX === undefined && caretXPositionLocator) { 449 currentX = caretXPositionLocator(); 450 } 451 452 if (isNaN(currentX)) { 453 // Return as the current X offset is unknown. Either no locator is set or the locator returned 454 // undefined (e.g., caret not currently visible). 455 return; 456 } 457 458 stepIterator = createKeyboardStepIterator(); 459 // Move to the start/end of the current line. 460 if (!guiStepUtils.moveToFilteredStep(stepIterator, direction, stepScanners)) { 461 // No line boundary found 462 return; 463 } 464 465 // Move to the first step on the next line 466 if (!stepIterator.advanceStep(direction)) { 467 // No step available in the specified direction 468 return; 469 } 470 471 stepScanners = [new gui.ClosestXOffsetScanner(/**@type{!number}*/(currentX)), 472 new gui.LineBoundaryScanner(), new gui.ParagraphBoundaryScanner()]; 473 // Finally, move to the closest point to the desired X offset within the current line 474 if (guiStepUtils.moveToFilteredStep(stepIterator, direction, stepScanners)) { 475 moveCursorFocusPoint(stepIterator.container(), stepIterator.offset(), extend); 476 lastXPosition = currentX; 477 resetLastXPositionTask.restart(); 478 } 479 } 480 481 /** 482 * @return {!boolean} 483 */ 484 function moveCursorUp() { 485 moveCursorByLine(PREVIOUS, false); 486 return true; 487 } 488 this.moveCursorUp = moveCursorUp; 489 490 /** 491 * @return {!boolean} 492 */ 493 function moveCursorDown() { 494 moveCursorByLine(NEXT, false); 495 return true; 496 } 497 this.moveCursorDown = moveCursorDown; 498 499 /** 500 * @return {!boolean} 501 */ 502 function extendSelectionUp() { 503 moveCursorByLine(PREVIOUS, true); 504 return true; 505 } 506 this.extendSelectionUp = extendSelectionUp; 507 508 /** 509 * @return {!boolean} 510 */ 511 function extendSelectionDown() { 512 moveCursorByLine(NEXT, true); 513 return true; 514 } 515 this.extendSelectionDown = extendSelectionDown; 516 517 /** 518 * @param {!core.StepDirection} direction 519 * @param {!boolean} extend 520 * @return {undefined} 521 */ 522 function moveCursorToLineBoundary(direction, extend) { 523 var stepIterator = createKeyboardStepIterator(), 524 stepScanners = [new gui.LineBoundaryScanner(), new gui.ParagraphBoundaryScanner()]; 525 526 // Both a line boundary AND a paragraph boundary scanner are necessary to ensure the caret stops correctly 527 // inside an empty paragraph. 528 // The line boundary scanner requires a visible client rect in order to detect a line break, but for an 529 // empty paragraph, there is no visible leading or trailing rect as there aren't any visible children. 530 // As a result, the line boundary detection can't determine if an empty paragraph is a line-wrap point, but 531 // the paragraph boundary scanner *will* correctly determine that step iterator has moved beyond the 532 // current paragraph. 533 if (guiStepUtils.moveToFilteredStep(stepIterator, direction, stepScanners)) { 534 moveCursorFocusPoint(stepIterator.container(), stepIterator.offset(), extend); 535 } 536 } 537 538 /** 539 * @param {!core.StepDirection} direction 540 * @param {!boolean} extend whether extend the selection instead of moving the cursor 541 * @return {undefined} 542 */ 543 function moveCursorByWord(direction, extend) { 544 var cursor = odtDocument.getCursor(inputMemberId), 545 newSelection = rangeToSelection(cursor.getSelectedRange(), cursor.hasForwardSelection()), 546 stepIterator = createWordBoundaryStepIterator(newSelection.focusNode, newSelection.focusOffset, TRAILING_SPACE); 547 548 if (stepIterator.advanceStep(direction)) { 549 moveCursorFocusPoint(stepIterator.container(), stepIterator.offset(), extend); 550 } 551 } 552 553 /** 554 * @return {!boolean} 555 */ 556 function moveCursorBeforeWord() { 557 moveCursorByWord(PREVIOUS, false); 558 return true; 559 } 560 this.moveCursorBeforeWord = moveCursorBeforeWord; 561 562 /** 563 * @return {!boolean} 564 */ 565 function moveCursorPastWord() { 566 moveCursorByWord(NEXT, false); 567 return true; 568 } 569 this.moveCursorPastWord = moveCursorPastWord; 570 571 /** 572 * @return {!boolean} 573 */ 574 function extendSelectionBeforeWord() { 575 moveCursorByWord(PREVIOUS, true); 576 return true; 577 } 578 this.extendSelectionBeforeWord = extendSelectionBeforeWord; 579 580 /** 581 * @return {!boolean} 582 */ 583 function extendSelectionPastWord() { 584 moveCursorByWord(NEXT, true); 585 return true; 586 } 587 this.extendSelectionPastWord = extendSelectionPastWord; 588 589 /** 590 * @return {!boolean} 591 */ 592 function moveCursorToLineStart() { 593 moveCursorToLineBoundary(PREVIOUS, false); 594 return true; 595 } 596 this.moveCursorToLineStart = moveCursorToLineStart; 597 598 /** 599 * @return {!boolean} 600 */ 601 function moveCursorToLineEnd() { 602 moveCursorToLineBoundary(NEXT, false); 603 return true; 604 } 605 this.moveCursorToLineEnd = moveCursorToLineEnd; 606 607 /** 608 * @return {!boolean} 609 */ 610 function extendSelectionToLineStart() { 611 moveCursorToLineBoundary(PREVIOUS, true); 612 return true; 613 } 614 this.extendSelectionToLineStart = extendSelectionToLineStart; 615 616 /** 617 * @return {!boolean} 618 */ 619 function extendSelectionToLineEnd() { 620 moveCursorToLineBoundary(NEXT, true); 621 return true; 622 } 623 this.extendSelectionToLineEnd = extendSelectionToLineEnd; 624 625 /** 626 * @param {!core.StepDirection} direction 627 * @param {!boolean} extend True to extend the selection 628 * @param {!function(!Node):Node} getContainmentNode Returns a node container for the supplied node. 629 * Usually this will be something like the parent paragraph or root the supplied node is within 630 * @return {undefined} 631 */ 632 function adjustSelectionByNode(direction, extend, getContainmentNode) { 633 var validStepFound = false, 634 cursor = odtDocument.getCursor(inputMemberId), 635 containmentNode, 636 selection = rangeToSelection(cursor.getSelectedRange(), cursor.hasForwardSelection()), 637 rootElement = odtDocument.getRootElement(selection.focusNode), 638 stepIterator; 639 640 runtime.assert(Boolean(rootElement), "SelectionController: Cursor outside root"); 641 stepIterator = odtDocument.createStepIterator(selection.focusNode, selection.focusOffset, [baseFilter, rootFilter], rootElement); 642 stepIterator.roundToClosestStep(); 643 644 if (!stepIterator.advanceStep(direction)) { 645 return; 646 } 647 648 containmentNode = getContainmentNode(stepIterator.container()); 649 if (!containmentNode) { 650 return; 651 } 652 653 if (direction === PREVIOUS) { 654 stepIterator.setPosition(/**@type{!Node}*/(containmentNode), 0); 655 // Round up to the first walkable step in the containment node 656 validStepFound = stepIterator.roundToNextStep(); 657 } else { 658 stepIterator.setPosition(/**@type{!Node}*/(containmentNode), containmentNode.childNodes.length); 659 // Round down to the last walkable step in the containment node 660 validStepFound = stepIterator.roundToPreviousStep(); 661 } 662 663 if (validStepFound) { 664 moveCursorFocusPoint(stepIterator.container(), stepIterator.offset(), extend); 665 } 666 } 667 668 /** 669 * @return {!boolean} 670 */ 671 this.extendSelectionToParagraphStart = function() { 672 adjustSelectionByNode(PREVIOUS, true, odfUtils.getParagraphElement); 673 return true; 674 }; 675 676 /** 677 * @return {!boolean} 678 */ 679 this.extendSelectionToParagraphEnd = function () { 680 adjustSelectionByNode(NEXT, true, odfUtils.getParagraphElement); 681 return true; 682 }; 683 684 /** 685 * @return {!boolean} 686 */ 687 this.moveCursorToParagraphStart = function () { 688 adjustSelectionByNode(PREVIOUS, false, odfUtils.getParagraphElement); 689 return true; 690 }; 691 692 /** 693 * @return {!boolean} 694 */ 695 this.moveCursorToParagraphEnd = function () { 696 adjustSelectionByNode(NEXT, false, odfUtils.getParagraphElement); 697 return true; 698 }; 699 700 /** 701 * @return {!boolean} 702 */ 703 this.moveCursorToDocumentStart = function () { 704 adjustSelectionByNode(PREVIOUS, false, odtDocument.getRootElement); 705 return true; 706 }; 707 708 /** 709 * @return {!boolean} 710 */ 711 this.moveCursorToDocumentEnd = function () { 712 adjustSelectionByNode(NEXT, false, odtDocument.getRootElement); 713 return true; 714 }; 715 716 /** 717 * @return {!boolean} 718 */ 719 this.extendSelectionToDocumentStart = function () { 720 adjustSelectionByNode(PREVIOUS, true, odtDocument.getRootElement); 721 return true; 722 }; 723 724 /** 725 * @return {!boolean} 726 */ 727 this.extendSelectionToDocumentEnd = function () { 728 adjustSelectionByNode(NEXT, true, odtDocument.getRootElement); 729 return true; 730 }; 731 732 /** 733 * @return {!boolean} 734 */ 735 function extendSelectionToEntireDocument() { 736 var cursor = odtDocument.getCursor(inputMemberId), 737 rootElement = odtDocument.getRootElement(cursor.getNode()), 738 anchorNode, 739 anchorOffset, 740 stepIterator, 741 newCursorSelection; 742 743 runtime.assert(Boolean(rootElement), "SelectionController: Cursor outside root"); 744 stepIterator = odtDocument.createStepIterator(rootElement, 0, [baseFilter, rootFilter], rootElement); 745 stepIterator.roundToClosestStep(); 746 anchorNode = stepIterator.container(); 747 anchorOffset = stepIterator.offset(); 748 749 stepIterator.setPosition(rootElement, rootElement.childNodes.length); 750 stepIterator.roundToClosestStep(); 751 newCursorSelection = odtDocument.convertDomToCursorRange({ 752 anchorNode: anchorNode, 753 anchorOffset: anchorOffset, 754 focusNode: stepIterator.container(), 755 focusOffset: stepIterator.offset() 756 }); 757 session.enqueue([createOpMoveCursor(newCursorSelection.position, newCursorSelection.length)]); 758 return true; 759 } 760 this.extendSelectionToEntireDocument = extendSelectionToEntireDocument; 761 762 /** 763 * @param {!function(!Error=)} callback passing an error object in case of error 764 * @return {undefined} 765 */ 766 this.destroy = function (callback) { 767 odtDocument.unsubscribe(ops.OdtDocument.signalOperationStart, resetLastXPosition); 768 core.Async.destroyAll([resetLastXPositionTask.destroy], callback); 769 }; 770 771 function init() { 772 resetLastXPositionTask = core.Task.createTimeoutTask(function() { 773 lastXPosition = undefined; 774 }, UPDOWN_NAVIGATION_RESET_DELAY_MS); 775 odtDocument.subscribe(ops.OdtDocument.signalOperationStart, resetLastXPosition); 776 } 777 init(); 778 }; 779