1 /**
  2  * Copyright (C) 2014 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*/
 26 
 27 
 28 /**
 29  * Creates a helper class for navigating by steps. Instances of this class are intended to be VERY
 30  * short-lived, and makes no guarantees about proper behaviour if the DOM or supplied filter is
 31  * modified during the lifetime of the object.
 32  *
 33  * @constructor
 34  * @param {!core.PositionFilter} filter Filter to apply to the iterator positions
 35  * @param {!core.PositionIterator} iterator Substree to search for step within. Generally a paragraph or document root
 36  */
 37 core.StepIterator = function StepIterator(filter, iterator) {
 38     "use strict";
 39 
 40     var /**@const*/
 41         FILTER_ACCEPT = core.PositionFilter.FilterResult.FILTER_ACCEPT,
 42         /**@const*/
 43         NEXT = core.StepDirection.NEXT,
 44         cachedContainer,
 45         cachedOffset,
 46         cachedFilterResult;
 47 
 48     function resetCache() {
 49         // TODO Speed up access of the container & offset pairs on the PositionIterator
 50         // These values are cached because container & offset lookups on the iterator
 51         // can be prohibitively slow. Ideally, the iterator itself will be eventually sped up
 52         cachedContainer = null;
 53         cachedOffset = undefined;
 54         cachedFilterResult = undefined;
 55     }
 56 
 57     /**
 58      * Returns true if the current iterator position is accepted by the supplied filter
 59      * @return {!boolean}
 60      */
 61     function isStep() {
 62         if (cachedFilterResult === undefined) {
 63             cachedFilterResult = filter.acceptPosition(iterator) === FILTER_ACCEPT;
 64         }
 65         return /**@type{!boolean}*/(cachedFilterResult);
 66     }
 67     this.isStep = isStep;
 68 
 69     /**
 70      * Sets the position of the underlying iterator
 71      * @param {!Node} newContainer
 72      * @param {!number} newOffset
 73      * @return {!boolean}
 74      */
 75     function setPosition(newContainer, newOffset) {
 76         resetCache();
 77         return iterator.setUnfilteredPosition(newContainer, newOffset);
 78     }
 79     this.setPosition = setPosition;
 80 
 81     /**
 82      * Return the container for the current position.
 83      * @return {!Element|!Text}
 84      */
 85      function container() {
 86         if (!cachedContainer) {
 87             cachedContainer = iterator.container();
 88         }
 89         return cachedContainer;
 90     }
 91     this.container = container;
 92 
 93     /**
 94      * Get the current unfiltered DOM offset of the underlying iterator
 95      * @return {!number}
 96      */
 97     function offset() {
 98         if (cachedOffset === undefined) {
 99             cachedOffset = iterator.unfilteredDomOffset();
100         }
101         return /**@type{!number}*/(cachedOffset);
102     }
103     this.offset = offset;
104 
105     /**
106      * Move to the next step. Returns false if no step exists
107      * @return {!boolean}
108      */
109     function nextStep() {
110         resetCache(); // Necessary in case the are no more positions
111         while (iterator.nextPosition()) {
112             resetCache();
113             if (isStep()) {
114                 return true;
115             }
116         }
117         return false;
118     }
119     this.nextStep = nextStep;
120 
121     /**
122      * Move to the previous step. Returns false if no step exists
123      * @return {!boolean}
124      */
125     function previousStep() {
126         resetCache(); // Necessary in case the are no more positions
127         while (iterator.previousPosition()) {
128             resetCache();
129             if (isStep()) {
130                 return true;
131             }
132         }
133         return false;
134     }
135     this.previousStep = previousStep;
136 
137     /**
138      * Advance the iterator by one step in the specified direction.
139      *
140      * @param {!core.StepDirection} direction
141      * @return {!boolean}
142      */
143     this.advanceStep = function(direction) {
144         return direction === NEXT ? nextStep() : previousStep();
145     };
146 
147     /**
148      * If the current position is not on a valid step, this function will move the iterator
149      * to the closest previous step. If there is no previous step, it will advance to the next
150      * closest step.
151      * @return {!boolean} Returns true if the iterator ends on a valid step
152      */
153     this.roundToClosestStep = function() {
154         var currentContainer,
155             currentOffset,
156             isAtStep = isStep();
157         if (!isAtStep) {
158             currentContainer = container();
159             currentOffset = offset();
160             // Default rule is to always round a position DOWN to the closest step equal or prior
161             // This produces the easiest behaviour to understand (e.g., put the cursor just AFTER the step it represents)
162             isAtStep = previousStep();
163             if (!isAtStep) {
164                 // Restore back to the prior position and see if there is a step available above
165                 setPosition(currentContainer, currentOffset);
166                 isAtStep = nextStep();
167             }
168         }
169         return isAtStep;
170     };
171 
172     /**
173      * If the current position is not a valid step, move to the previous step.
174      * If there is no previous step, returns false.
175      * @return {!boolean} Returns true if the iterator ends on a valid step
176      */
177     this.roundToPreviousStep = function() {
178         var isAtStep = isStep();
179         if (!isAtStep) {
180             isAtStep = previousStep();
181         }
182         return isAtStep;
183     };
184 
185     /**
186      * If the current position is not a valid step, move to the next step.
187      * If there is no next step, returns false.
188      * @return {!boolean} Returns true if the iterator ends on a valid step
189      */
190     this.roundToNextStep = function() {
191         var isAtStep = isStep();
192         if (!isAtStep) {
193             isAtStep = nextStep();
194         }
195         return isAtStep;
196     };
197 
198     /**
199      * Return the node to the left of the current iterator position.
200      * See PositionIterator.leftNode
201      * @return {?Node}
202      */
203     this.leftNode = function() {
204         return iterator.leftNode();
205     };
206 
207     /**
208      * Store a snapshot of the current step iterator position. Intended to be used
209      * in conjunction with restore to be able to save & restore a particular position.
210      *
211      * Note, the returned type should be treated as an opaque token, as the data structure
212      * is allowed to change at any moment.
213      *
214      * @return {!core.StepIterator.StepSnapshot}
215      */
216     this.snapshot = function() {
217         return new core.StepIterator.StepSnapshot(container(), offset());
218     };
219 
220     /**
221      * Restore the step iterator back to a specific position. The input to this is
222      * expected to be the direct result of a snapshot call.
223      *
224      * @param {!core.StepIterator.StepSnapshot} snapshot
225      * @return {undefined}
226      */
227     this.restore = function(snapshot) {
228         setPosition(snapshot.container, snapshot.offset);
229     };
230 };
231 
232 
233 /**
234  * StepIterator snapshot token that is used to save and restore the current position of StepIterator
235  *
236  * All properties and methods on this class are intended to be private to StepIterator, and should not be used outside
237  * of the StepIterator file. The contents stored may be changed at any time and should not be relied upon by
238  * external consumers.
239  *
240  * @constructor
241  * @param {!Text|!Element} container
242  * @param {!number} offset
243  */
244 core.StepIterator.StepSnapshot = function (container, offset) {
245     "use strict";
246 
247     /**
248      * @private
249      * @type {!Text|!Element}
250      */
251     this.container = container;
252 
253     /**
254      * @private
255      * @type {!number}
256      */
257     this.offset = offset;
258 };
259