braille_util.js revision 46d4c2bc3267f3f028f39e7e311b0f89aba2e4fd
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.
4
5/**
6 * @fileoverview A utility class for general braille functionality.
7 */
8
9
10goog.provide('cvox.BrailleUtil');
11
12goog.require('cvox.ChromeVox');
13goog.require('cvox.DomUtil');
14goog.require('cvox.Focuser');
15goog.require('cvox.NavBraille');
16goog.require('cvox.NodeStateUtil');
17goog.require('cvox.Spannable');
18
19
20/**
21 * Trimmable whitespace character that appears between consecutive items in
22 * braille.
23 * @const {string}
24 */
25cvox.BrailleUtil.ITEM_SEPARATOR = ' ';
26
27
28/**
29 * Messages considered as containers in braille.
30 * Containers are distinguished from roles by their appearance higher up in the
31 * DOM tree of a selected node.
32 * This list should be very short.
33 * @type {!Array.<string>}
34 */
35cvox.BrailleUtil.CONTAINER = [
36  'tag_h1_brl',
37  'tag_h2_brl',
38  'tag_h3_brl',
39  'tag_h4_brl',
40  'tag_h5_brl',
41  'tag_h6_brl'
42];
43
44
45/**
46 * Maps a ChromeVox message id to a braille template.
47 * The template takes one-character specifiers:
48 * n: replaced with braille name.
49 * r: replaced with braille role.
50 * s: replaced with braille state.
51 * c: replaced with braille container role; this potentially returns whitespace,
52 * so place at the beginning or end of templates for trimming.
53 * v: replaced with braille value.
54 * @type {Object.<string, string>}
55 */
56cvox.BrailleUtil.TEMPLATE = {
57  'base': 'c n v r s',
58  'aria_role_alert': 'r: n',
59  'aria_role_button': '[n]',
60  'aria_role_textbox': 'n: v r',
61  'input_type_button': '[n]',
62  'input_type_checkbox': 'n (s)',
63  'input_type_email': 'n: v r',
64  'input_type_number': 'n: v r',
65  'input_type_password': 'n: v r',
66  'input_type_search': 'n: v r',
67  'input_type_submit': '[n]',
68  'input_type_text': 'n: v r',
69  'input_type_tel': 'n: v r',
70  'input_type_url': 'n: v r',
71  'tag_button': '[n]',
72  'tag_textarea': 'n: v r'
73};
74
75
76/**
77 * Attached to the value region of a braille spannable.
78 * @param {number} offset The offset of the span into the value.
79 * @constructor
80 * @struct
81 */
82cvox.BrailleUtil.ValueSpan = function(offset) {
83  /**
84   * The offset of the span into the value.
85   * @type {number}
86   */
87  this.offset = offset;
88};
89
90
91/**
92 * Creates a value span from a json serializable object.
93 * @param {!Object} obj The json serializable object to convert.
94 * @return {!cvox.BrailleUtil.ValueSpan}
95 */
96cvox.BrailleUtil.ValueSpan.fromJson = function(obj) {
97  return new cvox.BrailleUtil.ValueSpan(obj.offset);
98};
99
100
101/**
102 * Converts this object to a json serializable object.
103 * @return {!Object}
104 */
105cvox.BrailleUtil.ValueSpan.prototype.toJson = function() {
106  return this;
107};
108
109
110cvox.Spannable.registerSerializableSpan(
111    cvox.BrailleUtil.ValueSpan,
112    'cvox.BrailleUtil.ValueSpan',
113    cvox.BrailleUtil.ValueSpan.fromJson,
114    cvox.BrailleUtil.ValueSpan.prototype.toJson);
115
116
117/**
118 * Attached to the selected text within a value.
119 * @constructor
120 * @struct
121 */
122cvox.BrailleUtil.ValueSelectionSpan = function() {
123};
124
125
126cvox.Spannable.registerStatelessSerializableSpan(
127    cvox.BrailleUtil.ValueSelectionSpan,
128    'cvox.BrailleUtil.ValueSelectionSpan');
129
130
131/**
132 * Gets the braille name for a node.
133 * See DomUtil for a more precise definition of 'name'.
134 * Additionally, whitespace is trimmed.
135 * @param {Node} node The node.
136 * @return {string} The string representation.
137 */
138cvox.BrailleUtil.getName = function(node) {
139  if (!node) {
140    return '';
141  }
142  return cvox.DomUtil.getName(node).trim();
143};
144
145
146/**
147 * Gets the braille role message id for a node.
148 * See DomUtil for a more precise definition of 'role'.
149 * @param {Node} node The node.
150 * @return {string} The string representation.
151 */
152cvox.BrailleUtil.getRoleMsg = function(node) {
153  if (!node) {
154    return '';
155  }
156  var roleMsg = cvox.DomUtil.getRoleMsg(node, cvox.VERBOSITY_VERBOSE);
157  if (roleMsg) {
158    roleMsg = cvox.DomUtil.collapseWhitespace(roleMsg);
159  }
160  if (roleMsg && (roleMsg.length > 0)) {
161    if (cvox.ChromeVox.msgs.getMsg(roleMsg + '_brl')) {
162      roleMsg += '_brl';
163    }
164  }
165  return roleMsg;
166};
167
168
169/**
170 * Gets the braille role of a node.
171 * See DomUtil for a more precise definition of 'role'.
172 * @param {Node} node The node.
173 * @return {string} The string representation.
174 */
175cvox.BrailleUtil.getRole = function(node) {
176  if (!node) {
177    return '';
178  }
179  var roleMsg = cvox.BrailleUtil.getRoleMsg(node);
180  return roleMsg ? cvox.ChromeVox.msgs.getMsg(roleMsg) : '';
181};
182
183
184/**
185 * Gets the braille state of a node.
186 * @param {Node} node The node.
187 * @return {string} The string representation.
188 */
189cvox.BrailleUtil.getState = function(node) {
190  if (!node) {
191    return '';
192  }
193  return cvox.NodeStateUtil.expand(
194      cvox.DomUtil.getStateMsgs(node, true).map(function(state) {
195          // Check to see if a variant of the message with '_brl' exists,
196          // and use it if so.
197          //
198          // Note: many messages are templatized, and if we don't pass any
199          // argument to substitute, getMsg might throw an error if the
200          // resulting string is empty. To avoid this, we pass a dummy
201          // substitution string array here.
202          var dummySubs = ['dummy', 'dummy', 'dummy'];
203          if (cvox.ChromeVox.msgs.getMsg(state[0] + '_brl', dummySubs)) {
204            state[0] += '_brl';
205          }
206          return state;
207      }));
208};
209
210
211/**
212 * Gets the braille container role of a node.
213 * @param {Node} prev The previous node in navigation.
214 * @param {Node} node The node.
215 * @return {string} The string representation.
216 */
217cvox.BrailleUtil.getContainer = function(prev, node) {
218  if (!prev || !node) {
219    return '';
220  }
221  var ancestors = cvox.DomUtil.getUniqueAncestors(prev, node);
222  for (var i = 0, container; container = ancestors[i]; i++) {
223    var msg = cvox.BrailleUtil.getRoleMsg(container);
224    if (msg && cvox.BrailleUtil.CONTAINER.indexOf(msg) != -1) {
225      return cvox.ChromeVox.msgs.getMsg(msg);
226    }
227  }
228  return '';
229};
230
231
232/**
233 * Gets the braille value of a node. A cvox.BrailleUtil.ValueSpan will be
234 * attached, along with (possibly) a cvox.BrailleUtil.ValueSelectionSpan.
235 * @param {Node} node The node.
236 * @return {!cvox.Spannable} The value spannable.
237 */
238cvox.BrailleUtil.getValue = function(node) {
239  if (!node) {
240    return new cvox.Spannable();
241  }
242  var valueSpan = new cvox.BrailleUtil.ValueSpan(0 /* offset */);
243  if (cvox.DomUtil.isInputTypeText(node)) {
244    var value = node.value;
245    if (node.type === 'password') {
246      value = value.replace(/./g, '*');
247    }
248    var spannable = new cvox.Spannable(value, valueSpan);
249    if (node === document.activeElement &&
250        cvox.DomUtil.doesInputSupportSelection(node)) {
251      var selectionStart = cvox.BrailleUtil.clamp_(
252          node.selectionStart, 0, spannable.getLength());
253      var selectionEnd = cvox.BrailleUtil.clamp_(
254          node.selectionEnd, 0, spannable.getLength());
255      spannable.setSpan(new cvox.BrailleUtil.ValueSelectionSpan(),
256                        Math.min(selectionStart, selectionEnd),
257                        Math.max(selectionStart, selectionEnd));
258    }
259    return spannable;
260  } else if (node instanceof HTMLTextAreaElement) {
261    var shadow = new cvox.EditableTextAreaShadow();
262    shadow.update(node);
263    var lineIndex = shadow.getLineIndex(node.selectionEnd);
264    var lineStart = shadow.getLineStart(lineIndex);
265    var lineEnd = shadow.getLineEnd(lineIndex);
266    var lineText = node.value.substring(lineStart, lineEnd);
267    valueSpan.offset = lineStart;
268    var spannable = new cvox.Spannable(lineText, valueSpan);
269    if (node === document.activeElement) {
270      var selectionStart = cvox.BrailleUtil.clamp_(
271          node.selectionStart - lineStart, 0, spannable.getLength());
272      var selectionEnd = cvox.BrailleUtil.clamp_(
273          node.selectionEnd - lineStart, 0, spannable.getLength());
274      spannable.setSpan(new cvox.BrailleUtil.ValueSelectionSpan(),
275                        Math.min(selectionStart, selectionEnd),
276                        Math.max(selectionStart, selectionEnd));
277    }
278    return spannable;
279  } else {
280    return new cvox.Spannable(cvox.DomUtil.getValue(node), valueSpan);
281  }
282};
283
284
285/**
286 * Gets the templated representation of braille.
287 * @param {Node} prev The previous node (during navigation).
288 * @param {Node} node The node.
289 * @param {{name:(undefined|string),
290 * role:(undefined|string),
291 * roleMsg:(undefined|string),
292 * state:(undefined|string),
293 * container:(undefined|string),
294 * value:(undefined|cvox.Spannable)}|Object=} opt_override Override a
295 * specific property for the given node.
296 * @return {!cvox.Spannable} The string representation.
297 */
298cvox.BrailleUtil.getTemplated = function(prev, node, opt_override) {
299  opt_override = opt_override ? opt_override : {};
300  var roleMsg = opt_override.roleMsg ||
301      (node ? cvox.DomUtil.getRoleMsg(node, cvox.VERBOSITY_VERBOSE) : '');
302  var role = opt_override.role;
303  if (!role && opt_override.roleMsg) {
304    role = cvox.ChromeVox.msgs.getMsg(opt_override.roleMsg + '_brl') ||
305        cvox.ChromeVox.msgs.getMsg(opt_override.roleMsg);
306  }
307  role = role || cvox.BrailleUtil.getRole(node);
308  var template = cvox.BrailleUtil.TEMPLATE[roleMsg] ||
309      cvox.BrailleUtil.TEMPLATE['base'];
310
311  var templated = new cvox.Spannable();
312  var mapChar = function(c) {
313    switch (c) {
314      case 'n':
315        return opt_override.name || cvox.BrailleUtil.getName(node);
316      case 'r':
317        return role;
318      case 's':
319        return opt_override.state || cvox.BrailleUtil.getState(node);
320      case 'c':
321        return opt_override.container ||
322            cvox.BrailleUtil.getContainer(prev, node);
323      case 'v':
324        return opt_override.value || cvox.BrailleUtil.getValue(node);
325      default:
326        return c;
327    }
328  };
329  for (var i = 0; i < template.length; i++) {
330    var component = mapChar(template[i]);
331    templated.append(component);
332    // Ignore the next whitespace separator if the current component is empty.
333    if (!component.toString() && template[i + 1] == ' ') {
334      i++;
335    }
336  }
337  return templated.trimRight();
338};
339
340
341/**
342 * Creates a braille value from a string and, optionally, a selection range.
343 * A cvox.BrailleUtil.ValueSpan will be
344 * attached, along with a cvox.BrailleUtil.ValueSelectionSpan if applicable.
345 * @param {string} text The text to display as the value.
346 * @param {number=} opt_selStart Selection start.
347 * @param {number=} opt_selEnd Selection end if different from selection start.
348 * @param {number=} opt_textOffset Start offset of text.
349 * @return {!cvox.Spannable} The value spannable.
350 */
351cvox.BrailleUtil.createValue = function(text, opt_selStart, opt_selEnd,
352                                        opt_textOffset) {
353  var spannable = new cvox.Spannable(
354      text, new cvox.BrailleUtil.ValueSpan(opt_textOffset || 0));
355  if (goog.isDef(opt_selStart)) {
356    opt_selEnd = goog.isDef(opt_selEnd) ? opt_selEnd : opt_selStart;
357    // TODO(plundblad): This looses the distinction between the selection
358    // anchor (start) and focus (end).  We should use that information to
359    // decide where to pan the braille display.
360    if (opt_selStart > opt_selEnd) {
361      var temp = opt_selStart;
362      opt_selStart = opt_selEnd;
363      opt_selEnd = temp;
364    }
365
366    spannable.setSpan(new cvox.BrailleUtil.ValueSelectionSpan(),
367          opt_selStart, opt_selEnd);
368  }
369  return spannable;
370};
371
372
373/**
374 * Activates a position in a nav braille.  Moves the caret in text fields
375 * and simulates a mouse click on the node at the position.
376 *
377 * @param {!cvox.NavBraille} braille the nav braille representing the display
378 *        content that was active when the user issued the key command.
379 *        The annotations in the spannable are used to decide what
380 *        node to activate and what part of the node value (if any) to
381 *        move the caret to.
382 * @param {number=} opt_displayPosition position of the display that the user
383 *                  activated, relative to the start of braille.
384 */
385cvox.BrailleUtil.click = function(braille, opt_displayPosition) {
386  var spans = braille.text.getSpans(opt_displayPosition || 0);
387  var node = spans.filter(function(n) { return n instanceof Node; })[0];
388  if (node) {
389    cvox.Focuser.setFocus(node);
390    if (goog.isDef(opt_displayPosition) &&
391        (cvox.DomUtil.isInputTypeText(node) ||
392            node instanceof HTMLTextAreaElement)) {
393      var valueSpan = spans.filter(
394          function(s) {
395            return s instanceof cvox.BrailleUtil.ValueSpan;
396          })[0];
397      if (valueSpan) {
398        var cursorPosition = opt_displayPosition -
399            braille.text.getSpanStart(valueSpan) +
400            valueSpan.offset;
401        cvox.ChromeVoxEventWatcher.setUpTextHandler();
402        node.selectionStart = node.selectionEnd = cursorPosition;
403        cvox.ChromeVoxEventWatcher.handleTextChanged(true);
404      }
405    }
406  }
407  cvox.DomUtil.clickElem(node ||
408      cvox.ChromeVox.navigationManager.getCurrentNode(), false, false);
409};
410
411
412/**
413 * Clamps a number so it is within the given boundaries.
414 * @param {number} number The number to clamp.
415 * @param {number} min The minimum value to return.
416 * @param {number} max The maximum value to return.
417 * @return {number} {@code number} if it is within the bounds, or the nearest
418 *     number within the bounds otherwise.
419 * @private
420 */
421cvox.BrailleUtil.clamp_ = function(number, min, max) {
422  return Math.min(Math.max(number, min), max);
423};
424