Skip to content

Commit

Permalink
[ChromeVox] Create file editable_text.js
Browse files Browse the repository at this point in the history
AX-Relnotes: n/a.
Bug: b/265481751
Test: ChromeVoxEditingTest.*
Change-Id: Ib43ffa009e19d6484d27665d1026931a8e53db2d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4833958
Reviewed-by: Akihiro Ota <akihiroota@chromium.org>
Auto-Submit: Anastasia Helfinstein <anastasi@google.com>
Commit-Queue: Akihiro Ota <akihiroota@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1194188}
  • Loading branch information
anastasi-google authored and Chromium LUCI CQ committed Sep 8, 2023
1 parent f773863 commit 5e8b640
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 171 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ chromevox_es6_modules = [
"background/earcon_engine.js",
"background/earcons.js",
"background/editing/editable_line.js",
"background/editing/editable_text.js",
"background/editing/editable_text_base.js",
"background/editing/editing.js",
"background/editing/intent_handler.js",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {NavBraille} from '../../common/braille/nav_braille.js';
import {Msgs} from '../../common/msgs.js';
import {Spannable} from '../../common/spannable.js';
import {ValueSelectionSpan, ValueSpan} from '../braille/spans.js';
import {ChromeVox} from '../chromevox.js';
import {OutputNodeSpan} from '../output/output_types.js';

import {ChromeVoxEditableTextBase, TextChangeEvent} from './editable_text_base.js';

const AutomationIntent = chrome.automation.AutomationIntent;
const AutomationNode = chrome.automation.AutomationNode;
const StateType = chrome.automation.StateType;

/**
* A |ChromeVoxEditableTextBase| that implements text editing feedback
* for automation tree text fields.
*/
export class AutomationEditableText extends ChromeVoxEditableTextBase {
/** @param {!AutomationNode} node */
constructor(node) {
if (!node.state.editable) {
throw Error('Node must have editable state set to true.');
}
const value = AutomationEditableText.getProcessedValue_(node) ?? '';
const lineBreaks = AutomationEditableText.getLineBreaks_(value);
const start = node.textSelStart;
const end = node.textSelEnd;

super(
value, Math.min(start, end, value.length),
Math.min(Math.max(start, end), value.length),
node.state[StateType.PROTECTED] /**password*/, ChromeVox.tts);
/** @private {!Array<number>} */
this.lineBreaks_ = lineBreaks;
/** @override */
this.multiline = node.state[StateType.MULTILINE] || false;
/** @protected {!AutomationNode} */
this.node_ = node;
}

/**
* Called when the text field has been updated.
* @param {!Array<AutomationIntent>} intents
*/
onUpdate(intents) {
const oldValue = this.value;
const oldStart = this.start;
const oldEnd = this.end;
const newValue =
AutomationEditableText.getProcessedValue_(this.node_) ?? '';
if (oldValue !== newValue) {
this.lineBreaks_ = AutomationEditableText.getLineBreaks_(newValue);
}

const textChangeEvent = new TextChangeEvent(
newValue, Math.min(this.node_.textSelStart ?? 0, newValue.length),
Math.min(this.node_.textSelEnd ?? 0, newValue.length),
true /* triggered by user */);
this.changed(textChangeEvent);
this.outputBraille_(oldValue, oldStart, oldEnd);
}

/**
* Returns true if selection starts on the first line.
*/
isSelectionOnFirstLine() {
return this.getLineIndex(this.start) === 0;
}

/**
* Returns true if selection ends on the last line.
*/
isSelectionOnLastLine() {
return this.getLineIndex(this.end) >= this.lineBreaks_.length - 1;
}

/** @override */
getLineIndex(charIndex) {
let lineIndex = 0;
while (charIndex > this.lineBreaks_[lineIndex]) {
lineIndex++;
}
return lineIndex;
}

/** @override */
getLineStart(lineIndex) {
if (lineIndex === 0) {
return 0;
}

// The start of this line is defined as the line break of the previous line
// + 1 (the hard line break).
return this.lineBreaks_[lineIndex - 1] + 1;
}

/** @override */
getLineEnd(lineIndex) {
return this.lineBreaks_[lineIndex];
}

/** @private */
getLineIndexForBrailleOutput_(oldStart) {
let lineIndex = this.getLineIndex(this.start);
// Output braille at the end of the selection that changed, if start and end
// differ.
if (this.start !== this.end && this.start === oldStart) {
lineIndex = this.getLineIndex(this.end);
}
return lineIndex;
}

/** @private */
getTextFromIndexAndStart_(lineIndex, lineStart) {
const lineEnd = this.getLineEnd(lineIndex);
let lineText = this.value.substr(lineStart, lineEnd - lineStart);

if (lineIndex === 0) {
const textFieldTypeMsg =
Msgs.getMsg(this.multiline ? 'tag_textarea_brl' : 'role_textbox_brl');
lineText += ' ' + textFieldTypeMsg;
}

return lineText;
}

/** @private */
outputBraille_(oldValue, oldStart, oldEnd) {
const lineIndex = this.getLineIndexForBrailleOutput_(oldStart);
const lineStart = this.getLineStart(lineIndex);
let lineText = this.getTextFromIndexAndStart_(lineIndex, lineStart);

const startIndex = this.start - lineStart;
const endIndex = this.end - lineStart;

// If the line is not the last line, and is empty, insert an explicit line
// break so that braille output is correctly cleared and has a position for
// a caret to be shown.
if (lineText === '' && lineIndex < this.lineBreaks_.length - 1) {
lineText = '\n';
}

const value = new Spannable(lineText, new OutputNodeSpan(this.node_));
value.setSpan(new ValueSpan(0), 0, lineText.length);
value.setSpan(new ValueSelectionSpan(), startIndex, endIndex);
ChromeVox.braille.write(
new NavBraille({text: value, startIndex, endIndex}));
}

/**
* @param {!AutomationNode} node
* @return {string|undefined}
* @private
*/
static getProcessedValue_(node) {
let value = node.value;
if (node.inputType === 'tel') {
value = value?.trimEnd();
}
return value;
}

/**
* @param {string} value
* @return {!Array<number>}
* @private
*/
static getLineBreaks_(value) {
const lineBreaks = [];
const lines = value.split('\n');
let total = 0;
for (let i = 0; i < lines.length; i++) {
total += lines[i].length;
lineBreaks[i] = total;

// Account for the line break itself.
total++;
}
return lineBreaks;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ import {ChromeVoxRange, ChromeVoxRangeObserver} from '../chromevox_range.js';
import {ChromeVoxState} from '../chromevox_state.js';
import {Color} from '../color.js';
import {Output} from '../output/output.js';
import {OutputCustomEvent, OutputNodeSpan} from '../output/output_types.js';
import {OutputCustomEvent} from '../output/output_types.js';

import {EditableLine} from './editable_line.js';
import {AutomationEditableText} from './editable_text.js';
import {ChromeVoxEditableTextBase, TextChangeEvent} from './editable_text_base.js';
import {IntentHandler} from './intent_handler.js';
import {TextEditHandler} from './text_edit_handler.js';
Expand All @@ -39,175 +40,6 @@ const IntentCommandType = chrome.automation.IntentCommandType;
const RoleType = chrome.automation.RoleType;
const StateType = chrome.automation.StateType;

/**
* A |ChromeVoxEditableTextBase| that implements text editing feedback
* for automation tree text fields.
*/
export class AutomationEditableText extends ChromeVoxEditableTextBase {
/** @param {!AutomationNode} node */
constructor(node) {
if (!node.state.editable) {
throw Error('Node must have editable state set to true.');
}
const value = AutomationEditableText.getProcessedValue_(node) ?? '';
const lineBreaks = AutomationEditableText.getLineBreaks_(value);
const start = node.textSelStart;
const end = node.textSelEnd;

super(
value, Math.min(start, end, value.length),
Math.min(Math.max(start, end), value.length),
node.state[StateType.PROTECTED] /**password*/, ChromeVox.tts);
/** @private {!Array<number>} */
this.lineBreaks_ = lineBreaks;
/** @override */
this.multiline = node.state[StateType.MULTILINE] || false;
/** @type {!AutomationNode} @private */
this.node_ = node;
}

/**
* Called when the text field has been updated.
* @param {!Array<AutomationIntent>} intents
*/
onUpdate(intents) {
const oldValue = this.value;
const oldStart = this.start;
const oldEnd = this.end;
const newValue =
AutomationEditableText.getProcessedValue_(this.node_) ?? '';
if (oldValue !== newValue) {
this.lineBreaks_ = AutomationEditableText.getLineBreaks_(newValue);
}

const textChangeEvent = new TextChangeEvent(
newValue, Math.min(this.node_.textSelStart ?? 0, newValue.length),
Math.min(this.node_.textSelEnd ?? 0, newValue.length),
true /* triggered by user */);
this.changed(textChangeEvent);
this.outputBraille_(oldValue, oldStart, oldEnd);
}

/**
* Returns true if selection starts on the first line.
*/
isSelectionOnFirstLine() {
return this.getLineIndex(this.start) === 0;
}

/**
* Returns true if selection ends on the last line.
*/
isSelectionOnLastLine() {
return this.getLineIndex(this.end) >= this.lineBreaks_.length - 1;
}

/** @override */
getLineIndex(charIndex) {
let lineIndex = 0;
while (charIndex > this.lineBreaks_[lineIndex]) {
lineIndex++;
}
return lineIndex;
}

/** @override */
getLineStart(lineIndex) {
if (lineIndex === 0) {
return 0;
}

// The start of this line is defined as the line break of the previous line
// + 1 (the hard line break).
return this.lineBreaks_[lineIndex - 1] + 1;
}

/** @override */
getLineEnd(lineIndex) {
return this.lineBreaks_[lineIndex];
}

/** @private */
getLineIndexForBrailleOutput_(oldStart) {
let lineIndex = this.getLineIndex(this.start);
// Output braille at the end of the selection that changed, if start and end
// differ.
if (this.start !== this.end && this.start === oldStart) {
lineIndex = this.getLineIndex(this.end);
}
return lineIndex;
}

/** @private */
getTextFromIndexAndStart_(lineIndex, lineStart) {
const lineEnd = this.getLineEnd(lineIndex);
let lineText = this.value.substr(lineStart, lineEnd - lineStart);

if (lineIndex === 0) {
const textFieldTypeMsg =
Msgs.getMsg(this.multiline ? 'tag_textarea_brl' : 'role_textbox_brl');
lineText += ' ' + textFieldTypeMsg;
}

return lineText;
}

/** @private */
outputBraille_(oldValue, oldStart, oldEnd) {
const lineIndex = this.getLineIndexForBrailleOutput_(oldStart);
const lineStart = this.getLineStart(lineIndex);
let lineText = this.getTextFromIndexAndStart_(lineIndex, lineStart);

const startIndex = this.start - lineStart;
const endIndex = this.end - lineStart;

// If the line is not the last line, and is empty, insert an explicit line
// break so that braille output is correctly cleared and has a position for
// a caret to be shown.
if (lineText === '' && lineIndex < this.lineBreaks_.length - 1) {
lineText = '\n';
}

const value = new Spannable(lineText, new OutputNodeSpan(this.node_));
value.setSpan(new ValueSpan(0), 0, lineText.length);
value.setSpan(new ValueSelectionSpan(), startIndex, endIndex);
ChromeVox.braille.write(
new NavBraille({text: value, startIndex, endIndex}));
}

/**
* @param {!AutomationNode} node
* @return {string|undefined}
* @private
*/
static getProcessedValue_(node) {
let value = node.value;
if (node.inputType === 'tel') {
value = value?.trimEnd();
}
return value;
}

/**
* @param {string} value
* @return {!Array<number>}
* @private
*/
static getLineBreaks_(value) {
const lineBreaks = [];
const lines = value.split('\n');
let total = 0;
for (let i = 0; i < lines.length; i++) {
total += lines[i].length;
lineBreaks[i] = total;

// Account for the line break itself.
total++;
}
return lineBreaks;
}
}

/**
* A |ChromeVoxEditableTextBase| that implements text editing feedback
* for automation tree text fields using anchor and focus selection.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {ChromeVoxEvent} from '../../common/custom_automation_event.js';
import {ChromeVoxState} from '../chromevox_state.js';

import {EditableLine} from './editable_line.js';
import {AutomationEditableText, AutomationRichEditableText} from './editing.js';
import {AutomationEditableText} from './editable_text.js';
import {AutomationRichEditableText} from './editing.js';

const AutomationIntent = chrome.automation.AutomationIntent;
const AutomationNode = chrome.automation.AutomationNode;
Expand Down

0 comments on commit 5e8b640

Please sign in to comment.