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