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 };