1 /** 2 * Copyright (C) 2012-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, ops, odf*/ 26 27 (function () { 28 "use strict"; 29 30 /** 31 * 32 * @constructor 33 * @param {!Element} rootNode 34 * @param {!core.PositionIterator} iterator 35 * @param {!core.PositionFilter} filter 36 * @param {!number} bucketSize Minimum number of steps between cache points 37 */ 38 ops.OdtStepsTranslator = function OdtStepsTranslator(rootNode, iterator, filter, bucketSize) { 39 var /**@type{!ops.StepsCache}*/ 40 stepsCache, 41 odfUtils = odf.OdfUtils, 42 domUtils = core.DomUtils, 43 /**@const*/ 44 FILTER_ACCEPT = core.PositionFilter.FilterResult.FILTER_ACCEPT, 45 /**@const*/ 46 PREVIOUS = core.StepDirection.PREVIOUS, 47 /**@const*/ 48 NEXT = core.StepDirection.NEXT; 49 50 /** 51 * Update the steps cache based on the current iterator position. This can either add new 52 * bookmarks or update existing references and repair damaged regions of the cache. 53 * 54 * @param {!number} steps 55 * @param {!core.PositionIterator} iterator 56 * @param {!boolean} isStep 57 * @return {undefined} 58 */ 59 function updateCache(steps, iterator, isStep) { 60 var node = iterator.getCurrentNode(); 61 62 if (iterator.isBeforeNode() && odfUtils.isParagraph(node)) { 63 if (!isStep) { 64 // Paragraph bookmarks indicate "first position in the paragraph" 65 // If the current stable point is before the first walkable position (as often happens) 66 // simply increase the step number by 1 to move to within the paragraph node 67 steps += 1; 68 } 69 stepsCache.updateBookmark(steps, node); 70 } 71 } 72 73 /** 74 * Saved bookmarks always represent the first step inside the corresponding paragraph or node. Based on the 75 * current TextPositionFilter impl, this means rounding up if the current iterator position is not on a step. 76 * @param {!number} steps 77 * @param {!core.PositionIterator} iterator 78 * @return {undefined} 79 */ 80 function roundUpToStep(steps, iterator) { 81 do { 82 if (filter.acceptPosition(iterator) === FILTER_ACCEPT) { 83 // Have reached the step represented by the paragraph bookmark 84 updateCache(steps, iterator, true); 85 break; 86 } 87 // This logic inverts the +1 logic in updateCache. Conceptually speaking, the stored 88 // bookmark represents the first step in the paragraph. Until the first step is found, 89 // the iterator is still technically on steps-1. 90 updateCache(steps - 1, iterator, false); 91 } while (iterator.nextPosition()); 92 } 93 94 /** 95 * Convert the requested steps from root into the equivalent DOM node & offset pair. If the 96 * requested step is before the start or past the end of the document, a RangeError will be thrown. 97 * @param {!number} steps 98 * @return {!{node: !Node, offset: !number}} 99 */ 100 this.convertStepsToDomPoint = function (steps) { 101 var /**@type{!number}*/ 102 stepsFromRoot, 103 isStep; 104 105 if (isNaN(steps)) { 106 throw new TypeError("Requested steps is not numeric (" + steps + ")"); 107 } 108 if (steps < 0) { 109 throw new RangeError("Requested steps is negative (" + steps + ")"); 110 } 111 stepsFromRoot = stepsCache.setToClosestStep(steps, iterator); 112 113 while (stepsFromRoot < steps && iterator.nextPosition()) { 114 isStep = filter.acceptPosition(iterator) === FILTER_ACCEPT; 115 if (isStep) { 116 stepsFromRoot += 1; 117 } 118 updateCache(stepsFromRoot, iterator, isStep); 119 } 120 if (stepsFromRoot !== steps) { 121 throw new RangeError("Requested steps (" + steps + ") exceeds available steps (" + stepsFromRoot + ")"); 122 } 123 return { 124 node: iterator.container(), 125 offset: iterator.unfilteredDomOffset() 126 }; 127 }; 128 129 /** 130 * Uses the provided delegate to choose between rounding up or rounding down to the nearest step. 131 * @param {!core.PositionIterator} iterator 132 * @param {function(!core.StepDirection, !Node, !number):boolean=} roundDirection 133 * @return {!boolean} Returns true if an accepted position is found, otherwise returns false. 134 */ 135 function roundToPreferredStep(iterator, roundDirection) { 136 if (!roundDirection || filter.acceptPosition(iterator) === FILTER_ACCEPT) { 137 return true; 138 } 139 140 while (iterator.previousPosition()) { 141 if (filter.acceptPosition(iterator) === FILTER_ACCEPT) { 142 if (roundDirection(PREVIOUS, iterator.container(), iterator.unfilteredDomOffset())) { 143 return true; 144 } 145 break; 146 } 147 } 148 149 while (iterator.nextPosition()) { 150 if (filter.acceptPosition(iterator) === FILTER_ACCEPT) { 151 if (roundDirection(NEXT, iterator.container(), iterator.unfilteredDomOffset())) { 152 return true; 153 } 154 break; 155 } 156 } 157 158 return false; 159 } 160 161 /** 162 * Convert the supplied DOM node & offset pair into it's equivalent steps from root 163 * If the node & offset is not in an accepted location, the 164 * roundDirection delegate is used to choose between rounding up or 165 * rounding down to the nearest step. If not provided, the default 166 * behaviour is to round down. 167 * @param {!Node} node 168 * @param {!number} offset 169 * @param {function(!core.StepDirection, !Node, !number):!boolean=} roundDirection 170 * @return {!number} 171 */ 172 this.convertDomPointToSteps = function (node, offset, roundDirection) { 173 var stepsFromRoot, 174 beforeRoot, 175 destinationNode, 176 destinationOffset, 177 rounding = 0, 178 isStep; 179 180 if (!domUtils.containsNode(rootNode, node)) { 181 beforeRoot = domUtils.comparePoints(rootNode, 0, node, offset) < 0; 182 node = /**@type{!Node}*/(rootNode); 183 offset = beforeRoot ? 0 : /**@type{!Element}*/(rootNode).childNodes.length; 184 } 185 186 iterator.setUnfilteredPosition(node, offset); 187 // if the user has set provided a rounding selection delegate, use that to select the previous or next 188 // step if the (node, offset) position is not accepted by the filter 189 if (!roundToPreferredStep(iterator, roundDirection)) { 190 // The rounding selection delegate rejected both. Revert back to the previous step 191 iterator.setUnfilteredPosition(node, offset); 192 } 193 194 // Get the iterator equivalent position of the current node & offset 195 // This ensures the while loop will match the exact container and offset during iteration 196 destinationNode = iterator.container(); 197 destinationOffset = iterator.unfilteredDomOffset(); 198 199 stepsFromRoot = stepsCache.setToClosestDomPoint(destinationNode, destinationOffset, iterator); 200 if (domUtils.comparePoints(iterator.container(), iterator.unfilteredDomOffset(), destinationNode, destinationOffset) < 0) { 201 // Special case: the requested DOM point is between the bookmark node and walkable step it represents 202 return stepsFromRoot > 0 ? stepsFromRoot - 1 : stepsFromRoot; 203 } 204 205 while (!(iterator.container() === destinationNode && iterator.unfilteredDomOffset() === destinationOffset) 206 && iterator.nextPosition()) { 207 isStep = filter.acceptPosition(iterator) === FILTER_ACCEPT; 208 if (isStep) { 209 stepsFromRoot += 1; 210 } 211 updateCache(stepsFromRoot, iterator, isStep); 212 } 213 return stepsFromRoot + rounding; 214 }; 215 216 /** 217 * Iterates over all available positions starting at the root node and primes the cache 218 * @return {undefined} 219 */ 220 this.prime = function () { 221 var stepsFromRoot, 222 isStep; 223 224 stepsFromRoot = stepsCache.setToClosestStep(0, iterator); 225 while (iterator.nextPosition()) { 226 isStep = filter.acceptPosition(iterator) === FILTER_ACCEPT; 227 if (isStep) { 228 stepsFromRoot += 1; 229 } 230 updateCache(stepsFromRoot, iterator, isStep); 231 } 232 }; 233 234 /** 235 * @param {!{position: !number}} eventArgs 236 * @return {undefined} 237 */ 238 this.handleStepsInserted = function (eventArgs) { 239 // Old position = position 240 // New position = position + length 241 // E.g., {position: 10, length: 1} indicates 10 => 10, New => 11, 11 => 12, 12 => 13 242 stepsCache.damageCacheAfterStep(eventArgs.position); 243 }; 244 245 /** 246 * @param {!{position: !number}} eventArgs 247 * @return {undefined} 248 */ 249 this.handleStepsRemoved = function (eventArgs) { 250 // Old position = position + length 251 // New position = position 252 // E.g., {position: 10, length: 1} indicates 10 => 10, 11 => 10, 12 => 11 253 254 // TODO OpRemoveText inaccurately reports the position making it necessary subtract 1 255 // Paragraph merge behaviours might result in the paragraph exactly at the reported position being 256 // replaced by a later paragraph. Conceptually, this means the last unmodified position is 257 // actually 1 step prior to the replace paragraph. 258 stepsCache.damageCacheAfterStep(eventArgs.position - 1); 259 }; 260 261 function init() { 262 stepsCache = new ops.StepsCache(rootNode, bucketSize, roundUpToStep); 263 } 264 init(); 265 }; 266 }()); 267