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 Node, NodeFilter, runtime, core, gui, odf*/
 26 
 27 /**@typedef{{
 28   convertForwardStepsBetweenFilters:function(number,!core.PositionFilter,!core.PositionFilter):number,
 29   convertBackwardStepsBetweenFilters:function(number,!core.PositionFilter,!core.PositionFilter):number,
 30   countLinesSteps:function(number,!core.PositionFilter):number,
 31   countStepsToLineBoundary:function(number,!core.PositionFilter):number
 32 }}*/
 33 gui.StepCounter;
 34 
 35 /**
 36  * This class modifies the selection in different ways.
 37  * @constructor
 38  * @param {core.Cursor} cursor
 39  * @param {!Node} rootNode
 40  */
 41 gui.SelectionMover = function SelectionMover(cursor, rootNode) {
 42     "use strict";
 43     var odfUtils = new odf.OdfUtils(),
 44         /**@type{!core.PositionIterator}*/
 45         positionIterator,
 46         /**@const*/
 47         FILTER_ACCEPT = core.PositionFilter.FilterResult.FILTER_ACCEPT;
 48 
 49     /**
 50      * Resets the positionIterator back to the current cursor position and
 51      * returns the iterator.
 52      * @return {!core.PositionIterator}
 53      */
 54     function getIteratorAtCursor() {
 55         // This call relies on setUnfilteredPosition magic. After this call, the
 56         // iterator position will be just after the cursor because a position
 57         // in the cursor is not allowed. So this only works because the filter
 58         // in this instance of PositionIterator disallows positions in the
 59         // cursor.
 60         positionIterator.setUnfilteredPosition(cursor.getNode(), 0);
 61         return positionIterator;
 62     }
 63 
 64     /**
 65      * Gets the maximum available offset for a given node. For a text node, this
 66      * is text length, for element nodes, this will be childNodes.length
 67      * @param {!Node} node
 68      * @return {!number}
 69      */
 70     function getMaximumNodePosition(node) {
 71         return node.nodeType === Node.TEXT_NODE ? node.textContent.length : node.childNodes.length;
 72     }
 73 
 74     /**
 75      * Get the first or last client rectangle based on the useRightEdge flag.
 76      * If useRightEdge is set to true, this will return the right-most offset of
 77      * the last available rectangle
 78      * @param {ClientRectList} clientRectangles
 79      * @param {!boolean} useRightEdge
 80      * @return {?{top: !number, left: !number, bottom: !number}}
 81      */
 82     function getClientRect(clientRectangles, useRightEdge) {
 83         var rectangle,
 84             simplifiedRectangle = null;
 85 
 86         if (clientRectangles && clientRectangles.length > 0) {
 87             rectangle = useRightEdge ? clientRectangles.item(clientRectangles.length - 1) : clientRectangles.item(0);
 88         }
 89 
 90         if (rectangle) {
 91             simplifiedRectangle = {
 92                 top: rectangle.top,
 93                 left: useRightEdge ? rectangle.right : rectangle.left,
 94                 bottom: rectangle.bottom
 95             };
 96         }
 97         return simplifiedRectangle;
 98     }
 99 
100     /**
101      * Gets the client rect of a position specified by the container and an
102      * offset. If this is not possible with a range, then the last element's
103      * coordinates are used to guesstimate the position.
104      * @param {!Node} container
105      * @param {!number} offset
106      * @param {!Range} range
107      * @param {boolean=} useRightEdge Default value is false. Used when
108      *                                searching for the closest visually
109      *                                equivalent rectangle, starting at the
110      *                                specified container offset. In these
111      *                                circumstances, the right-side of the last
112      *                                client rectangle actually defines the
113      *                                visual position.
114      * @return {{top: !number, left: !number, bottom: !number}}
115      */
116     function getVisibleRect(container, offset, range, useRightEdge) {
117         var rectangle,
118             nodeType = container.nodeType;
119 
120         // There are various places where the list of client rects will be empty:
121         // - Empty text nodes
122         // - Non-visible elements (e.g., collapsed or wrapped whitespace, hidden elements etc.)
123         // - Non-visible coordinates (e.g., the last position of a paragraph is a selection position, but not a rendered character)
124         // In each case, we need to find the visually equivalent rectangle just preceding this container+offset
125 
126         // Step 1 - Select just the collapsed point
127         range.setStart(container, offset);
128         range.collapse(!useRightEdge);
129         rectangle = getClientRect(range.getClientRects(), useRightEdge === true);
130 
131         if (!rectangle && offset > 0) {
132             // Fallback 1 - Select the offset & preceding if available
133             range.setStart(container, offset - 1);
134             range.setEnd(container, offset);
135             rectangle = getClientRect(range.getClientRects(), true);
136         }
137 
138         if (!rectangle) {
139             if (nodeType === Node.ELEMENT_NODE && offset > 0
140                     && /**@type{!Element}*/(container).childNodes.length >= offset) {
141                 // Fallback 2 - there are other child nodes directly preceding this offset. Try those
142                 rectangle = getVisibleRect(container, offset - 1, range, true);
143             } else if (container.nodeType === Node.TEXT_NODE && offset > 0) {
144                 // Fallback 3 - this is a text node, so is either in an invisible container, or is collapsed whitespace, see if an adjacent character is visible
145                 rectangle = getVisibleRect(container, offset - 1, range, true);
146             } else if (container.previousSibling) {
147                 // Fallback 4 - Has a previous sibling, try using that
148                 rectangle = getVisibleRect(container.previousSibling, getMaximumNodePosition(container.previousSibling), range, true);
149             } else if (container.parentNode && container.parentNode !== rootNode) {
150                 // Fallback 5 - try using the parent container. Had no previous siblings, so look for the first offset
151                 rectangle = getVisibleRect(container.parentNode, 0, range, false);
152             } else {
153                 // Fallback 6 - no previous siblings have been found, try and return the root node's bounding container
154                 // Assert container === rootNode
155                 range.selectNode(rootNode);
156                 rectangle = getClientRect(range.getClientRects(), false);
157             }
158         }
159         runtime.assert(Boolean(rectangle), "No visible rectangle found");
160         return /**@type{{top: !number, left: !number, bottom: !number}}*/(rectangle);
161     }
162 
163     /**
164      * Returns the number of positions to the right the (steps, filter1) pair
165      * is equivalent to in filter2 space.
166      * @param {!number} stepsFilter1 Number of filter1 steps to count
167      * @param {!core.PositionFilter} filter1
168      * @param {!core.PositionFilter} filter2
169      * @return {!number} Equivalent steps in filter2 space
170      */
171     function convertForwardStepsBetweenFilters(stepsFilter1, filter1, filter2) {
172         var iterator = getIteratorAtCursor(),
173             watch = new core.LoopWatchDog(10000),
174             pendingStepsFilter2 = 0,
175             stepsFilter2 = 0;
176         while (stepsFilter1 > 0 && iterator.nextPosition()) {
177             watch.check();
178             if (filter2.acceptPosition(iterator) === FILTER_ACCEPT) {
179                 pendingStepsFilter2 += 1;
180                 if (filter1.acceptPosition(iterator) === FILTER_ACCEPT) {
181                     stepsFilter2 += pendingStepsFilter2;
182                     pendingStepsFilter2 = 0;
183                     stepsFilter1 -= 1;
184                 }
185             }
186         }
187         return stepsFilter2;
188     }
189     /**
190      * Returns the number of positions to the left the (steps, filter1) pair
191      * is equivalent to in filter2 space.
192      * @param {!number} stepsFilter1 Number of filter1 steps to count
193      * @param {!core.PositionFilter} filter1
194      * @param {!core.PositionFilter} filter2
195      * @return {!number} Equivalent steps in filter2 space
196      */
197     function convertBackwardStepsBetweenFilters(stepsFilter1, filter1, filter2) {
198         var iterator = getIteratorAtCursor(),
199             watch = new core.LoopWatchDog(10000),
200             pendingStepsFilter2 = 0,
201             stepsFilter2 = 0;
202         while (stepsFilter1 > 0 && iterator.previousPosition()) {
203             watch.check();
204             if (filter2.acceptPosition(iterator) === FILTER_ACCEPT) {
205                 pendingStepsFilter2 += 1;
206                 if (filter1.acceptPosition(iterator) === FILTER_ACCEPT) {
207                     stepsFilter2 += pendingStepsFilter2;
208                     pendingStepsFilter2 = 0;
209                     stepsFilter1 -= 1;
210                 }
211             }
212         }
213         return stepsFilter2;
214     }
215 
216     /**
217      * Return the number of steps needed to move across one line in the specified direction.
218      * If it is not possible to move across one line, then 0 is returned.
219      *
220      * @param {!number} direction -1 for upwards, +1 for downwards
221      * @param {!core.PositionFilter} filter
222      * @param {!core.PositionIterator} iterator
223      * @return {!number} steps
224      */
225     function countLineSteps(filter, direction, iterator) {
226         var c = iterator.container(),
227             steps = 0,
228             bestContainer = null,
229             bestOffset,
230             bestXDiff = 10,
231             xDiff,
232             bestCount = 0,
233             top,
234             left,
235             lastTop,
236             rect,
237             range = /**@type{!Range}*/(rootNode.ownerDocument.createRange()),
238             watch = new core.LoopWatchDog(10000);
239 
240         // Get the starting position
241         rect = getVisibleRect(c, iterator.unfilteredDomOffset(), range);
242 
243         top = rect.top;
244         left = rect.left;
245         lastTop = top;
246 
247         while ((direction < 0 ? iterator.previousPosition() : iterator.nextPosition()) === true) {
248             watch.check();
249             if (filter.acceptPosition(iterator) === FILTER_ACCEPT) {
250                 steps += 1;
251 
252                 c = iterator.container();
253                 rect = getVisibleRect(c, iterator.unfilteredDomOffset(), range);
254 
255                 if (rect.top !== top) { // Not on the initial line anymore
256                     if (rect.top !== lastTop && lastTop !== top) { // Not even on the next line
257                         break;
258                     }
259                     lastTop = rect.top;
260                     xDiff = Math.abs(left - rect.left);
261                     if (bestContainer === null || xDiff < bestXDiff) {
262                         bestContainer = c;
263                         bestOffset = iterator.unfilteredDomOffset();
264                         bestXDiff = xDiff;
265                         bestCount = steps;
266                     }
267                 }
268             }
269         }
270 
271         if (bestContainer !== null) {
272             iterator.setUnfilteredPosition(bestContainer, /**@type {!number}*/(bestOffset));
273             steps = bestCount;
274         } else {
275             steps = 0;
276         }
277 
278         range.detach();
279         return steps;
280     }
281     /**
282      * @param {!number} lines negative number for upwards, positive number for downwards
283      * @param {!core.PositionFilter} filter
284      * @return {!number} steps
285      */
286     function countLinesSteps(lines, filter) {
287         var iterator = getIteratorAtCursor(),
288             stepCount = 0,
289             steps = 0,
290             direction =  lines < 0 ? -1 : 1;
291 
292         lines = Math.abs(lines);
293         // move back in the document, until a position is found for which the
294         // top is smaller than initially and the left is closest
295         while (lines > 0) {
296             stepCount += countLineSteps(filter, direction, iterator);
297             if (stepCount === 0) {
298                 break;
299             }
300             steps += stepCount;
301             lines -= 1;
302         }
303         return steps * direction;
304     }
305     /**
306      * Returns the number of steps needed to move to the beginning/end of the
307      * line.
308      * @param {!number} direction -1 for beginning of the line, 1 for end of the
309      *                  line
310      * @param {!core.PositionFilter} filter
311      * @return {!number} steps
312      */
313     function countStepsToLineBoundary(direction, filter) {
314         var fnNextPos, increment,
315             lastRect, rect, onSameLine,
316             iterator = getIteratorAtCursor(),
317             paragraphNode = odfUtils.getParagraphElement(iterator.getCurrentNode()),
318             steps = 0,
319             range = /**@type{!Range}*/(rootNode.ownerDocument.createRange());
320 
321         if (direction < 0) {
322             fnNextPos = iterator.previousPosition;
323             increment = -1;
324         } else {
325             fnNextPos = iterator.nextPosition;
326             increment = 1;
327         }
328 
329         lastRect = getVisibleRect(iterator.container(), iterator.unfilteredDomOffset(), range);
330         while (fnNextPos.call(iterator)) {
331             if (filter.acceptPosition(iterator) === FILTER_ACCEPT) {
332                 // hit another paragraph node, so won't be the same line
333                 if (odfUtils.getParagraphElement(iterator.getCurrentNode()) !== paragraphNode) {
334                     break;
335                 }
336 
337                 rect = getVisibleRect(iterator.container(), iterator.unfilteredDomOffset(), range);
338                 if (rect.bottom !== lastRect.bottom) { // most cases it means hit the line above/below
339                     // if top and bottom overlaps, assume they are on the same line
340                     onSameLine = (rect.top >= lastRect.top && rect.bottom < lastRect.bottom)
341                         || (rect.top <= lastRect.top && rect.bottom > lastRect.bottom);
342                     if (!onSameLine) {
343                         break;
344                     }
345                 }
346 
347                 steps += increment;
348                 lastRect = rect;
349             }
350         }
351 
352         range.detach();
353         return steps;
354     }
355 
356     /**
357      * @return {!gui.StepCounter}
358      */
359     this.getStepCounter = function () {
360         return {
361             convertForwardStepsBetweenFilters: convertForwardStepsBetweenFilters,
362             convertBackwardStepsBetweenFilters: convertBackwardStepsBetweenFilters,
363             countLinesSteps: countLinesSteps,
364             countStepsToLineBoundary: countStepsToLineBoundary
365         };
366     };
367     function init() {
368         positionIterator = gui.SelectionMover.createPositionIterator(rootNode);
369         var range = rootNode.ownerDocument.createRange();
370         range.setStart(positionIterator.container(), positionIterator.unfilteredDomOffset());
371         range.collapse(true);
372         cursor.setSelectedRange(range);
373     }
374     init();
375 };
376 /**
377  * @param {!Node} rootNode
378  * @return {!core.PositionIterator}
379  */
380 gui.SelectionMover.createPositionIterator = function (rootNode) {
381     "use strict";
382     /**
383      * @constructor
384      * @extends NodeFilter
385       */
386     function CursorFilter() {
387         /**
388          * @param {?Node} node
389          * @return {!number}
390          */
391         this.acceptNode = function (node) {
392             if (!node || node.namespaceURI === "urn:webodf:names:cursor" ||
393                     node.namespaceURI === "urn:webodf:names:editinfo") {
394                 return NodeFilter.FILTER_REJECT;
395             }
396             return NodeFilter.FILTER_ACCEPT;
397         };
398     }
399     var filter = new CursorFilter();
400     return new core.PositionIterator(rootNode, 5, filter, false);
401 };
402