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         cachedContainer,
 43         cachedOffset,
 44         cachedFilterResult;
 45 
 46     function resetCache() {
 47         // TODO Speed up access of the container & offset pairs on the PositionIterator
 48         // These values are cached because container & offset lookups on the iterator
 49         // can be prohibitively slow. Ideally, the iterator itself will be eventually sped up
 50         cachedContainer = null;
 51         cachedOffset = undefined;
 52         cachedFilterResult = undefined;
 53     }
 54 
 55     /**
 56      * Returns true if the current iterator position is accepted by the supplied filter
 57      * @return {!boolean}
 58      */
 59     function isStep() {
 60         if (cachedFilterResult === undefined) {
 61             cachedFilterResult = filter.acceptPosition(iterator) === FILTER_ACCEPT;
 62         }
 63         return /**@type{!boolean}*/(cachedFilterResult);
 64     }
 65     this.isStep = isStep;
 66 
 67     /**
 68      * Sets the position of the underlying iterator
 69      * @param {!Node} newContainer
 70      * @param {!number} newOffset
 71      * @return {!boolean}
 72      */
 73     function setPosition(newContainer, newOffset) {
 74         resetCache();
 75         return iterator.setUnfilteredPosition(newContainer, newOffset);
 76     }
 77     this.setPosition = setPosition;
 78 
 79     /**
 80      * Return the container for the current position.
 81      * @return {!Element|!Text}
 82      */
 83      function container() {
 84         if (!cachedContainer) {
 85             cachedContainer = iterator.container();
 86         }
 87         return cachedContainer;
 88     }
 89     this.container = container;
 90 
 91     /**
 92      * Get the current unfiltered DOM offset of the underlying iterator
 93      * @return {!number}
 94      */
 95     function offset() {
 96         if (cachedOffset === undefined) {
 97             cachedOffset = iterator.unfilteredDomOffset();
 98         }
 99         return /**@type{!number}*/(cachedOffset);
100     }
101     this.offset = offset;
102 
103     /**
104      * Move to the next step. Returns false if no step exists
105      * @return {!boolean}
106      */
107     function nextStep() {
108         resetCache(); // Necessary in case the are no more positions
109         while (iterator.nextPosition()) {
110             resetCache();
111             if (isStep()) {
112                 return true;
113             }
114         }
115         return false;
116     }
117     this.nextStep = nextStep;
118 
119     /**
120      * Move to the previous step. Returns false if no step exists
121      * @return {!boolean}
122      */
123     function previousStep() {
124         resetCache(); // Necessary in case the are no more positions
125         while (iterator.previousPosition()) {
126             resetCache();
127             if (isStep()) {
128                 return true;
129             }
130         }
131         return false;
132     }
133     this.previousStep = previousStep;
134 
135     /**
136      * Advance the iterator by one step in the specified direction.
137      *
138      * @param {!core.StepDirection} direction
139      * @return {!boolean}
140      */
141     this.advanceStep = function(direction) {
142         return direction === core.StepDirection.NEXT ? nextStep() : previousStep();
143     };
144 
145     /**
146      * If the current position is not on a valid step, this function will move the iterator
147      * to the closest previous step. If there is no previous step, it will advance to the next
148      * closest step.
149      * @return {!boolean} Returns true if the iterator ends on a valid step
150      */
151     this.roundToClosestStep = function() {
152         var currentContainer,
153             currentOffset,
154             isAtStep = isStep();
155         if (!isAtStep) {
156             currentContainer = container();
157             currentOffset = offset();
158             // Default rule is to always round a position DOWN to the closest step equal or prior
159             // This produces the easiest behaviour to understand (e.g., put the cursor just AFTER the step it represents)
160             isAtStep = previousStep();
161             if (!isAtStep) {
162                 // Restore back to the prior position and see if there is a step available above
163                 setPosition(currentContainer, currentOffset);
164                 isAtStep = nextStep();
165             }
166         }
167         return isAtStep;
168     };
169 
170     /**
171      * If the current position is not a valid step, move to the previous step.
172      * If there is no previous step, returns false.
173      * @return {!boolean} Returns true if the iterator ends on a valid step
174      */
175     this.roundToPreviousStep = function() {
176         var isAtStep = isStep();
177         if (!isAtStep) {
178             isAtStep = previousStep();
179         }
180         return isAtStep;
181     };
182 
183     /**
184      * If the current position is not a valid step, move to the next step.
185      * If there is no next step, returns false.
186      * @return {!boolean} Returns true if the iterator ends on a valid step
187      */
188     this.roundToNextStep = function() {
189         var isAtStep = isStep();
190         if (!isAtStep) {
191             isAtStep = nextStep();
192         }
193         return isAtStep;
194     };
195 
196     /**
197      * Return the node to the left of the current iterator position.
198      * See PositionIterator.leftNode
199      * @return {?Node}
200      */
201     this.leftNode = function() {
202         return iterator.leftNode();
203     };
204 };
205