Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / chromevox / common / braille_util.js
blob9ab921b0d2286ac84427b26f52b616996a8f9109
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 /**
6  * @fileoverview A utility class for general braille functionality.
7  */
10 goog.provide('cvox.BrailleUtil');
12 goog.require('cvox.ChromeVox');
13 goog.require('cvox.DomUtil');
14 goog.require('cvox.EditableTextAreaShadow');
15 goog.require('cvox.Focuser');
16 goog.require('cvox.NavBraille');
17 goog.require('cvox.NodeStateUtil');
18 goog.require('cvox.Spannable');
19 goog.require('cvox.ValueSelectionSpan');
20 goog.require('cvox.ValueSpan');
23 /**
24  * Trimmable whitespace character that appears between consecutive items in
25  * braille.
26  * @const {string}
27  */
28 cvox.BrailleUtil.ITEM_SEPARATOR = ' ';
31 /**
32  * Messages considered as containers in braille.
33  * Containers are distinguished from roles by their appearance higher up in the
34  * DOM tree of a selected node.
35  * This list should be very short.
36  * @type {!Array<string>}
37  */
38 cvox.BrailleUtil.CONTAINER = [
39   'tag_h1_brl',
40   'tag_h2_brl',
41   'tag_h3_brl',
42   'tag_h4_brl',
43   'tag_h5_brl',
44   'tag_h6_brl'
48 /**
49  * Maps a ChromeVox message id to a braille template.
50  * The template takes one-character specifiers:
51  * n: replaced with braille name.
52  * r: replaced with braille role.
53  * s: replaced with braille state.
54  * c: replaced with braille container role; this potentially returns whitespace,
55  * so place at the beginning or end of templates for trimming.
56  * v: replaced with braille value.
57  * @type {Object<string>}
58  */
59 cvox.BrailleUtil.TEMPLATE = {
60   'base': 'c n v r s',
61   'aria_role_alert': 'r: n',
62   'aria_role_button': 'n r s',
63   'aria_role_checkbox': 'n r s',
64   'aria_role_menuitemcheckbox': 'n r s',
65   'aria_role_menuitemradio': 'n r s',
66   'aria_role_radio': 'n r s',
67   'aria_role_textbox': 'n: v r s',
68   'input_type_button': 'n r s',
69   'input_type_checkbox': 'n r s',
70   'input_type_email': 'n: v r s',
71   'input_type_number': 'n: v r s',
72   'input_type_password': 'n: v r s',
73   'input_type_radio': 'n r s',
74   'input_type_search': 'n: v r s',
75   'input_type_submit': 'n r s',
76   'input_type_text': 'n: v r s',
77   'input_type_tel': 'n: v r s',
78   'input_type_url': 'n: v r s',
79   'tag_button': 'n r s',
80   'tag_textarea': 'n: v r s'
84 /**
85  * Gets the braille name for a node.
86  * See DomUtil for a more precise definition of 'name'.
87  * Additionally, whitespace is trimmed.
88  * @param {Node} node The node.
89  * @return {string} The string representation.
90  */
91 cvox.BrailleUtil.getName = function(node) {
92   if (!node) {
93     return '';
94   }
95   return cvox.DomUtil.getName(node).trim();
99 /**
100  * Gets the braille role message id for a node.
101  * See DomUtil for a more precise definition of 'role'.
102  * @param {Node} node The node.
103  * @return {string} The string representation.
104  */
105 cvox.BrailleUtil.getRoleMsg = function(node) {
106   if (!node) {
107     return '';
108   }
109   var roleMsg = cvox.DomUtil.getRoleMsg(node, cvox.VERBOSITY_VERBOSE);
110   if (roleMsg) {
111     roleMsg = cvox.DomUtil.collapseWhitespace(roleMsg);
112   }
113   if (roleMsg && (roleMsg.length > 0)) {
114     if (cvox.ChromeVox.msgs.getMsg(roleMsg + '_brl')) {
115       roleMsg += '_brl';
116     }
117   }
118   return roleMsg;
123  * Transforms a {@code cvox.NodeState} list of state messages to the
124  * corresponding messages for braille and expands them into a localized
125  * string suitable for output on a braille display.
126  * @param {cvox.NodeState} stateMsgs The states to expand.  The content of this
127  *     array is modified.
128  * @return {string} The string representation.
129  * @private
130  */
131 cvox.BrailleUtil.expandStateMsgs_ = function(stateMsgs) {
132   stateMsgs.forEach(function(state) {
133     // Check to see if a variant of the message with '_brl' exists,
134     // and use it if so.
135     //
136     // Note: many messages are templatized, and if we don't pass any
137     // argument to substitute, getMsg might throw an error if the
138     // resulting string is empty. To avoid this, we pass a dummy
139     // substitution string array here.
140     var dummySubs = ['dummy', 'dummy', 'dummy'];
141     if (cvox.ChromeVox.msgs.getMsg(state[0] + '_brl', dummySubs)) {
142       state[0] += '_brl';
143     }
144   });
145   return cvox.NodeStateUtil.expand(stateMsgs);
150  * Gets the braille container role of a node.
151  * @param {Node} prev The previous node in navigation.
152  * @param {Node} node The node.
153  * @return {string} The string representation.
154  */
155 cvox.BrailleUtil.getContainer = function(prev, node) {
156   if (!prev || !node) {
157     return '';
158   }
159   var ancestors = cvox.DomUtil.getUniqueAncestors(prev, node);
160   for (var i = 0, container; container = ancestors[i]; i++) {
161     var msg = cvox.BrailleUtil.getRoleMsg(container);
162     if (msg && cvox.BrailleUtil.CONTAINER.indexOf(msg) != -1) {
163       return cvox.ChromeVox.msgs.getMsg(msg);
164     }
165   }
166   return '';
171  * Gets the braille value of a node. A {@code cvox.ValueSpan} will be
172  * attached, along with (possibly) a {@code cvox.ValueSelectionSpan}.
173  * @param {Node} node The node.
174  * @return {!cvox.Spannable} The value spannable.
175  */
176 cvox.BrailleUtil.getValue = function(node) {
177   if (!node) {
178     return new cvox.Spannable();
179   }
180   var valueSpan = new cvox.ValueSpan(0 /* offset */);
181   if (cvox.DomUtil.isInputTypeText(node)) {
182     var value = node.value;
183     if (node.type === 'password') {
184       value = value.replace(/./g, '*');
185     }
186     var spannable = new cvox.Spannable(value, valueSpan);
187     if (node === document.activeElement &&
188         cvox.DomUtil.doesInputSupportSelection(node)) {
189       var selectionStart = cvox.BrailleUtil.clamp_(
190           node.selectionStart, 0, spannable.getLength());
191       var selectionEnd = cvox.BrailleUtil.clamp_(
192           node.selectionEnd, 0, spannable.getLength());
193       spannable.setSpan(new cvox.ValueSelectionSpan(),
194                         Math.min(selectionStart, selectionEnd),
195                         Math.max(selectionStart, selectionEnd));
196     }
197     return spannable;
198   } else if (node instanceof HTMLTextAreaElement) {
199     var shadow = new cvox.EditableTextAreaShadow();
200     shadow.update(node);
201     var lineIndex = shadow.getLineIndex(node.selectionEnd);
202     var lineStart = shadow.getLineStart(lineIndex);
203     var lineEnd = shadow.getLineEnd(lineIndex);
204     var lineText = node.value.substring(lineStart, lineEnd);
205     valueSpan.offset = lineStart;
206     var spannable = new cvox.Spannable(lineText, valueSpan);
207     if (node === document.activeElement) {
208       var selectionStart = cvox.BrailleUtil.clamp_(
209           node.selectionStart - lineStart, 0, spannable.getLength());
210       var selectionEnd = cvox.BrailleUtil.clamp_(
211           node.selectionEnd - lineStart, 0, spannable.getLength());
212       spannable.setSpan(new cvox.ValueSelectionSpan(),
213                         Math.min(selectionStart, selectionEnd),
214                         Math.max(selectionStart, selectionEnd));
215     }
216     return spannable;
217   } else {
218     return new cvox.Spannable(cvox.DomUtil.getValue(node), valueSpan);
219   }
224  * Gets the templated representation of braille.
225  * @param {Node} prev The previous node (during navigation).
226  * @param {Node} node The node.
227  * @param {{name:(undefined|string),
228  * role:(undefined|string),
229  * roleMsg:(undefined|string),
230  * state:(undefined|string),
231  * container:(undefined|string),
232  * value:(undefined|cvox.Spannable)}|Object=} opt_override Override a
233  * specific property for the given node.
234  * @return {!cvox.Spannable} The string representation.
235  */
236 cvox.BrailleUtil.getTemplated = function(prev, node, opt_override) {
237   opt_override = opt_override ? opt_override : {};
238   var roleMsg = opt_override.roleMsg ||
239       (node ? cvox.DomUtil.getRoleMsg(node, cvox.VERBOSITY_VERBOSE) : '');
240   var template = cvox.BrailleUtil.TEMPLATE[roleMsg] ||
241       cvox.BrailleUtil.TEMPLATE['base'];
242   var state = opt_override.state;
243   if (!state) {
244     if (node) {
245       state = cvox.BrailleUtil.expandStateMsgs_(
246           cvox.DomUtil.getStateMsgs(node, true));
247     } else {
248       state = '';
249     }
250   }
251   var role = opt_override.role || '';
252   if (!role && roleMsg) {
253     role = cvox.ChromeVox.msgs.getMsg(roleMsg + '_brl') ||
254         cvox.ChromeVox.msgs.getMsg(roleMsg);
255   }
257   var templated = new cvox.Spannable();
258   var mapChar = function(c) {
259     switch (c) {
260       case 'n':
261         return opt_override.name || cvox.BrailleUtil.getName(node);
262       case 'r':
263         return role;
264       case 's':
265         return state;
266       case 'c':
267         return opt_override.container ||
268             cvox.BrailleUtil.getContainer(prev, node);
269       case 'v':
270         return opt_override.value || cvox.BrailleUtil.getValue(node);
271       default:
272         return c;
273     }
274   };
275   for (var i = 0; i < template.length; i++) {
276     var component = mapChar(template[i]);
277     templated.append(component);
278     // Ignore the next whitespace separator if the current component is empty,
279     // unless the empty value has a selection, in which case the cursor
280     // should be placed on the empty space after the empty value.
281     if (!component.toString() && template[i + 1] == ' ' &&
282         (!(component instanceof cvox.Spannable) ||
283         !/**@type {cvox.Spannable}*/(component).getSpanInstanceOf(
284             cvox.ValueSelectionSpan))) {
285       i++;
286     }
287   }
288   return templated.trimRight();
293  * Creates a braille value from a string and, optionally, a selection range.
294  * A {@code cvox.ValueSpan} will be attached, along with a
295  * {@code cvox.ValueSelectionSpan} if applicable.
296  * @param {string} text The text to display as the value.
297  * @param {number=} opt_selStart Selection start.
298  * @param {number=} opt_selEnd Selection end if different from selection start.
299  * @param {number=} opt_textOffset Start offset of text.
300  * @return {!cvox.Spannable} The value spannable.
301  */
302 cvox.BrailleUtil.createValue = function(text, opt_selStart, opt_selEnd,
303                                         opt_textOffset) {
304   var spannable = new cvox.Spannable(
305       text, new cvox.ValueSpan(opt_textOffset || 0));
306   if (goog.isDef(opt_selStart)) {
307     opt_selEnd = goog.isDef(opt_selEnd) ? opt_selEnd : opt_selStart;
308     // TODO(plundblad): This looses the distinction between the selection
309     // anchor (start) and focus (end).  We should use that information to
310     // decide where to pan the braille display.
311     if (opt_selStart > opt_selEnd) {
312       var temp = opt_selStart;
313       opt_selStart = opt_selEnd;
314       opt_selEnd = temp;
315     }
317     spannable.setSpan(new cvox.ValueSelectionSpan(), opt_selStart, opt_selEnd);
318   }
319   return spannable;
324  * Activates a position in a nav braille.  Moves the caret in text fields
325  * and simulates a mouse click on the node at the position.
327  * @param {!cvox.NavBraille} braille the nav braille representing the display
328  *        content that was active when the user issued the key command.
329  *        The annotations in the spannable are used to decide what
330  *        node to activate and what part of the node value (if any) to
331  *        move the caret to.
332  * @param {number=} opt_displayPosition position of the display that the user
333  *                  activated, relative to the start of braille.
334  */
335 cvox.BrailleUtil.click = function(braille, opt_displayPosition) {
336   var handled = false;
337   var spans = braille.text.getSpans(opt_displayPosition || 0);
338   var node = spans.filter(function(n) { return n instanceof Node; })[0];
339   if (node) {
340     if (goog.isDef(opt_displayPosition) &&
341         (cvox.DomUtil.isInputTypeText(node) ||
342             node instanceof HTMLTextAreaElement)) {
343       var valueSpan = spans.filter(
344           function(s) {
345             return s instanceof cvox.ValueSpan;
346           })[0];
347       if (valueSpan) {
348         if (document.activeElement !== node) {
349           cvox.Focuser.setFocus(node);
350         }
351         var cursorPosition = opt_displayPosition -
352             braille.text.getSpanStart(valueSpan) +
353             valueSpan.offset;
354         cvox.ChromeVoxEventWatcher.setUpTextHandler();
355         node.selectionStart = node.selectionEnd = cursorPosition;
356         cvox.ChromeVoxEventWatcher.handleTextChanged(true);
357         handled = true;
358       }
359     }
360   }
361   if (!handled) {
362     cvox.DomUtil.clickElem(
363         node || cvox.ChromeVox.navigationManager.getCurrentNode(),
364         false, false, false, true);
365   }
370  * Clamps a number so it is within the given boundaries.
371  * @param {number} number The number to clamp.
372  * @param {number} min The minimum value to return.
373  * @param {number} max The maximum value to return.
374  * @return {number} {@code number} if it is within the bounds, or the nearest
375  *     number within the bounds otherwise.
376  * @private
377  */
378 cvox.BrailleUtil.clamp_ = function(number, min, max) {
379   return Math.min(Math.max(number, min), max);