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