1 /**
  2  * Copyright (C) 2010-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, odf, gui, core, Node*/
 26 
 27 /**
 28  * Helper functions to retrieve information about an ODF document using a step iterator
 29  * @constructor
 30  */
 31 gui.GuiStepUtils = function GuiStepUtils() {
 32     "use strict";
 33     var odfUtils = odf.OdfUtils,
 34         stepUtils = new odf.StepUtils(),
 35         domUtils = core.DomUtils,
 36         NEXT = core.StepDirection.NEXT,
 37         LEFT_TO_RIGHT = gui.StepInfo.VisualDirection.LEFT_TO_RIGHT,
 38         RIGHT_TO_LEFT = gui.StepInfo.VisualDirection.RIGHT_TO_LEFT;
 39 
 40     /**
 41      * Returns the client rectangle for the content bounds at the step iterator's current position.
 42      * Note, if the selected content is really collapsed whitespace, this function will return null.
 43      *
 44      * @param {!core.StepIterator} stepIterator
 45      * @return {?ClientRect}
 46      */
 47     function getContentRect(stepIterator) {
 48         var bounds = stepUtils.getContentBounds(stepIterator),
 49             range,
 50             rect = null;
 51 
 52         if (bounds) {
 53             if (bounds.container.nodeType === Node.TEXT_NODE) {
 54                 range = bounds.container.ownerDocument.createRange();
 55                 range.setStart(bounds.container, bounds.startOffset);
 56                 range.setEnd(bounds.container, bounds.endOffset);
 57                 // *MUST* use the BCR here rather than the individual client rects, as the individual client rects
 58                 // don't support subpixel accuracy. Most browsers *do* support subpixel values for the BCR though
 59                 // (FF, Chrome + IE!!)
 60                 rect = range.getClientRects().length > 0 ? range.getBoundingClientRect() : null;
 61                 if (rect
 62                     && /**@type{!Text}*/(bounds.container).data.substring(bounds.startOffset, bounds.endOffset) === " "
 63                     && rect.width <= 1) {
 64                     // In Chrome, collapsed whitespace still reports a width of 1px. In FF, they report as 0px.
 65                     // Consumers of this function are really wanting the cursor position for a given
 66                     // step, which will actually be the next step in this instance.
 67                     rect = null;
 68                 }
 69                 range.detach();
 70             } else if (odfUtils.isCharacterElement(bounds.container) || odfUtils.isCharacterFrame(bounds.container)) {
 71                 // Want to ignore some invisible document content elements such as annotation anchors.
 72                 rect = domUtils.getBoundingClientRect(bounds.container);
 73             }
 74         }
 75 
 76         return rect;
 77     }
 78     this.getContentRect = getContentRect;
 79 
 80     /**
 81      * Advance the step iterator in the specified direction until an accepted step is identified
 82      * by a token scanner.
 83      *
 84      * @param {!core.StepIterator} stepIterator
 85      * @param {!core.StepDirection} direction
 86      * @param {!Array.<!gui.VisualStepScanner>} scanners
 87      * @return {!boolean} Return true if a step was found that satisfied one of the scanners
 88      */
 89     function moveToFilteredStep(stepIterator, direction, scanners) {
 90         var isForward = direction === NEXT,
 91             leftRect,
 92             rightRect,
 93             previousRect,
 94             nextRect,
 95             /**@type{?core.StepIterator.StepSnapshot}*/
 96             destinationToken,
 97             // Just in case no destination is found, the iterator will reset back to the initial position
 98             initialToken = stepIterator.snapshot(),
 99             wasTerminated = false,
100             /**@type{!gui.StepInfo}*/
101             stepInfo;
102 
103         /**
104          * @param {!boolean} terminated
105          * @param {!gui.VisualStepScanner} scanner
106          * @return {!boolean};
107          */
108         function process(terminated, scanner) {
109             // Multiple token scanners might be complete in a single step
110             if (scanner.process(stepInfo, previousRect, nextRect)) {
111                 terminated = true;
112                 // A scanner might indicate iteration as complete without specifying a token
113                 // if no available steps exist in the specified direction.
114                 if (!destinationToken && scanner.token) {
115                     // Scanners that terminate the iteration get the first chance to specify the destination token
116                     destinationToken = scanner.token;
117                 }
118             }
119             return terminated;
120         }
121 
122         do {
123             // TODO Optimize performance by re-using the left/right rect from the last step (depending on direction)
124             leftRect = getContentRect(stepIterator);
125             stepInfo = /**@type{!gui.StepInfo}*/({
126                 token: stepIterator.snapshot(),
127                 container: stepIterator.container,
128                 offset: stepIterator.offset,
129                 direction: direction,
130                 // TODO account for right-to-left languages
131                 visualDirection: direction === NEXT ? LEFT_TO_RIGHT : RIGHT_TO_LEFT
132             });
133 
134             if (stepIterator.nextStep()) {
135                 rightRect = getContentRect(stepIterator);
136             } else {
137                 rightRect = null;
138             }
139             stepIterator.restore(stepInfo.token);
140 
141             if (isForward) {
142                 previousRect = leftRect;
143                 nextRect = rightRect;
144             } else {
145                 previousRect = rightRect;
146                 nextRect = leftRect;
147             }
148 
149             wasTerminated = scanners.reduce(process, false);
150         } while (!wasTerminated && stepIterator.advanceStep(direction));
151 
152         if (!wasTerminated) {
153             // If no token scanner has terminated the iteration, then check each
154             // token scanner for the last identified potential step
155             // and take the first specified token.
156             scanners.forEach(function(scanner) {
157                 if (!destinationToken && scanner.token) {
158                     destinationToken = scanner.token;
159                 }
160             });
161         }
162 
163         stepIterator.restore(destinationToken || initialToken);
164         return Boolean(destinationToken);
165     }
166     this.moveToFilteredStep = moveToFilteredStep;
167 };