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