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