| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // Constant to indicate selection index is not set. |
| const NO_SELECT_INDEX = -1; |
| |
| /** |
| * Class to handle navigating text. Currently, only |
| * navigation and selection in editable text fields is supported. |
| */ |
| class TextNavigationManager { |
| /** @param {!NavigationManager} navigationManager */ |
| constructor(navigationManager) { |
| /** @private {!NavigationManager} */ |
| this.navigationManager_ = navigationManager; |
| |
| /** @private {number} */ |
| this.selectionStartIndex_ = NO_SELECT_INDEX; |
| |
| /** @private {number} */ |
| this.selectionEndIndex_ = NO_SELECT_INDEX; |
| |
| /** @private {chrome.automation.AutomationNode} */ |
| this.selectionStartObject_; |
| |
| /** @private {chrome.automation.AutomationNode} */ |
| this.selectionEndObject_; |
| |
| /** @private {boolean} */ |
| this.currentlySelecting_ = false; |
| |
| /** @private {function(chrome.automation.AutomationEvent): undefined} */ |
| this.selectionListener_ = this.onNavChange_.bind(this); |
| } |
| |
| /** |
| * Jumps to the beginning of the text field (does nothing |
| * if already at the beginning). |
| * @public |
| */ |
| jumpToBeginning() { |
| if (this.currentlySelecting_) |
| this.setupDynamicSelection_(false /* resetCursor */); |
| EventHelper.simulateKeyPress(EventHelper.KeyCode.HOME, {ctrl: true}); |
| } |
| |
| /** |
| * Jumps to the end of the text field (does nothing if |
| * already at the end). |
| * @public |
| */ |
| jumpToEnd() { |
| if (this.currentlySelecting_) |
| this.setupDynamicSelection_(false /* resetCursor */); |
| EventHelper.simulateKeyPress(EventHelper.KeyCode.END, {ctrl: true}); |
| } |
| |
| /** |
| * Moves the text caret one character back (does nothing |
| * if there are no more characters preceding the current |
| * location of the caret). |
| * @public |
| */ |
| moveBackwardOneChar() { |
| if (this.currentlySelecting_) |
| this.setupDynamicSelection_(true /* resetCursor */); |
| EventHelper.simulateKeyPress(EventHelper.KeyCode.LEFT_ARROW); |
| } |
| |
| /** |
| * Moves the text caret one character forward (does nothing |
| * if there are no more characters following the current |
| * location of the caret). |
| * @public |
| */ |
| moveForwardOneChar() { |
| if (this.currentlySelecting_) |
| this.setupDynamicSelection_(true /* resetCursor */); |
| EventHelper.simulateKeyPress(EventHelper.KeyCode.RIGHT_ARROW); |
| } |
| |
| /** |
| * Moves the text caret one word backwards (does nothing |
| * if already at the beginning of the field). If the |
| * text caret is in the middle of a word, moves the caret |
| * to the beginning of that word. |
| * @public |
| */ |
| moveBackwardOneWord() { |
| if (this.currentlySelecting_) |
| this.setupDynamicSelection_(false /* resetCursor */); |
| EventHelper.simulateKeyPress(EventHelper.KeyCode.LEFT_ARROW, {ctrl: true}); |
| } |
| |
| /** |
| * Moves the text caret one word forward (does nothing if |
| * already at the end of the field). If the text caret is |
| * in the middle of a word, moves the caret to the end of |
| * that word. |
| * @public |
| */ |
| moveForwardOneWord() { |
| if (this.currentlySelecting_) |
| this.setupDynamicSelection_(false /* resetCursor */); |
| EventHelper.simulateKeyPress(EventHelper.KeyCode.RIGHT_ARROW, {ctrl: true}); |
| } |
| |
| /** |
| * Moves the text caret one line up (does nothing |
| * if there are no lines above the current location of |
| * the caret). |
| * @public |
| */ |
| moveUpOneLine() { |
| if (this.currentlySelecting_) |
| this.setupDynamicSelection_(true /* resetCursor */); |
| EventHelper.simulateKeyPress(EventHelper.KeyCode.UP_ARROW); |
| } |
| |
| /** |
| * Moves the text caret one line down (does nothing |
| * if there are no lines below the current location of |
| * the caret). |
| * @public |
| */ |
| moveDownOneLine() { |
| if (this.currentlySelecting_) |
| this.setupDynamicSelection_(true /* resetCursor */); |
| EventHelper.simulateKeyPress(EventHelper.KeyCode.DOWN_ARROW); |
| } |
| |
| /** |
| * TODO(crbug.com/999400): Work on text selection dynamic highlight and |
| * text selection implementation below |
| */ |
| |
| /** |
| * Sets up the cursor position and selection listener for dynamic selection. |
| * If the needToResetCursor boolean is true, the function will move the cursor |
| * to the end point of the selection before adding the event listener. If not, |
| * it will simply add the listener. |
| * @param {boolean} needToResetCursor |
| * @private |
| */ |
| setupDynamicSelection_(needToResetCursor) { |
| if (needToResetCursor) { |
| if (this.currentlySelecting() && |
| this.selectionEndIndex_ != NO_SELECT_INDEX) { |
| // Move the cursor to the end of the existing selection. |
| chrome.automation.setDocumentSelection({ |
| anchorObject: this.selectionEndObject_, |
| anchorOffset: this.selectionEndIndex_, |
| focusObject: this.selectionEndObject_, |
| focusOffset: this.selectionEndIndex_ |
| }); |
| } |
| } |
| this.manageNavigationListener_(true /** Add the listener */); |
| } |
| |
| /** |
| * Sets the selection using the selectionStart and selectionEnd |
| * as the offset input for setDocumentSelection and the parameter |
| * textNode as the object input for setDocumentSelection. |
| * @private |
| */ |
| saveSelection_() { |
| if (this.selectionStartIndex_ == NO_SELECT_INDEX || |
| this.selectionEndIndex_ == NO_SELECT_INDEX) { |
| console.log( |
| 'Selection bounds are not set properly:', this.selectionStartIndex_, |
| this.selectionEndIndex_); |
| } else { |
| chrome.automation.setDocumentSelection({ |
| anchorObject: this.selectionStartObject_, |
| anchorOffset: this.selectionStartIndex_, |
| focusObject: this.selectionEndObject_, |
| focusOffset: this.selectionEndIndex_ |
| }); |
| } |
| } |
| |
| /** |
| * Returns the selection end index. |
| * @return {number} |
| * @public |
| */ |
| getSelEndIndex() { |
| return this.selectionEndIndex_; |
| } |
| |
| /** |
| * Reset the selectionStartIndex to NO_SELECT_INDEX. |
| */ |
| resetSelStartIndex() { |
| this.selectionStartIndex_ = NO_SELECT_INDEX; |
| } |
| |
| /** |
| * Returns the selection start index. |
| * @return {number} |
| * @public |
| */ |
| getSelStartIndex() { |
| return this.selectionStartIndex_; |
| } |
| |
| /** |
| * Sets the selection start index. |
| * @param {number} startIndex |
| * @param {!chrome.automation.AutomationNode} textNode |
| * @public |
| */ |
| setSelStartIndexAndNode(startIndex, textNode) { |
| this.selectionStartIndex_ = startIndex; |
| this.selectionStartObject_ = textNode; |
| } |
| |
| /** |
| * Returns if the selection start index is set in the current node. |
| * @return {boolean} |
| * @public |
| */ |
| currentlySelecting() { |
| return ( |
| this.selectionStartIndex_ !== NO_SELECT_INDEX && |
| this.currentlySelecting_); |
| } |
| |
| /** |
| * Returns either the selection start index or the selection end index of the |
| * node based on the getStart param. |
| * @param {!chrome.automation.AutomationNode} node |
| * @param {boolean} getStart |
| * @return {number} selection start if getStart is true otherwise selection |
| * end |
| * @private |
| */ |
| getSelectionIndexFromNode_(node, getStart) { |
| let indexFromNode = NO_SELECT_INDEX; |
| if (getStart) { |
| indexFromNode = node.textSelStart; |
| } else { |
| indexFromNode = node.textSelEnd; |
| } |
| if (indexFromNode === undefined) { |
| return NO_SELECT_INDEX; |
| } |
| return indexFromNode; |
| } |
| |
| /** |
| * Sets the selectionStart variable based on the selection of the current |
| * node. Also sets the currently selecting boolean to true. |
| * @public |
| */ |
| saveSelectStart() { |
| chrome.automation.getFocus((focusedNode) => { |
| this.selectionStartObject_ = focusedNode; |
| this.selectionStartIndex_ = this.getSelectionIndexFromNode_( |
| this.selectionStartObject_, |
| true /* We are getting the start index.*/); |
| this.currentlySelecting_ = true; |
| }); |
| } |
| |
| /** |
| * Function to handle changes in the cursor position during selection. |
| * This function will remove the selection listener and set the end of the |
| * selection based on the new position. |
| * @private |
| */ |
| onNavChange_() { |
| this.manageNavigationListener_(false); |
| if (this.currentlySelecting) |
| this.saveSelectEnd(); |
| } |
| |
| /** |
| * Adds or removes the selection listener based on a boolean parameter. |
| * @param {boolean} addListener |
| * @private |
| */ |
| manageNavigationListener_(addListener) { |
| if (addListener) { |
| this.selectionStartObject_.addEventListener( |
| chrome.automation.EventType.TEXT_SELECTION_CHANGED, |
| this.selectionListener_, false /** Don't use capture.*/); |
| } else { |
| this.selectionStartObject_.removeEventListener( |
| chrome.automation.EventType.TEXT_SELECTION_CHANGED, |
| this.selectionListener_, false /** Don't use capture.*/); |
| } |
| } |
| |
| /** |
| * Reset the currentlySelecting variable to false, reset the selection |
| * indices, and remove the listener on navigation. |
| * @public |
| */ |
| resetCurrentlySelecting() { |
| this.currentlySelecting_ = false; |
| this.manageNavigationListener_(false /** Removing listener */); |
| this.selectionStartIndex_ = NO_SELECT_INDEX; |
| this.selectionEndIndex_ = NO_SELECT_INDEX; |
| } |
| |
| /** |
| * Sets the selectionEnd variable based on the selection of the current node. |
| * @public |
| */ |
| saveSelectEnd() { |
| chrome.automation.getFocus((focusedNode) => { |
| this.selectionEndObject_ = focusedNode; |
| this.selectionEndIndex_ = this.getSelectionIndexFromNode_( |
| this.selectionEndObject_, |
| false /*We are not getting the start index.*/); |
| this.saveSelection_(); |
| }); |
| } |
| } |