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