Home | History | Annotate | Download | only in common
      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 
     10 goog.provide('cvox.BrailleUtil');
     11 
     12 goog.require('cvox.ChromeVox');
     13 goog.require('cvox.DomUtil');
     14 goog.require('cvox.Focuser');
     15 goog.require('cvox.NavBraille');
     16 goog.require('cvox.NodeStateUtil');
     17 goog.require('cvox.Spannable');
     18 
     19 
     20 /**
     21  * Trimmable whitespace character that appears between consecutive items in
     22  * braille.
     23  * @const {string}
     24  */
     25 cvox.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  */
     35 cvox.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  */
     56 cvox.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  */
     81 cvox.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  */
     95 cvox.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  */
    104 cvox.BrailleUtil.ValueSpan.prototype.toJson = function() {
    105   return this;
    106 };
    107 
    108 
    109 cvox.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  */
    120 cvox.BrailleUtil.ValueSelectionSpan = function() {
    121 };
    122 
    123 
    124 cvox.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  */
    136 cvox.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  */
    150 cvox.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  */
    173 cvox.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  */
    187 cvox.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  */
    215 cvox.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  */
    236 cvox.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  */
    296 cvox.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  */
    349 cvox.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  */
    383 cvox.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  */
    426 cvox.BrailleUtil.clamp_ = function(number, min, max) {
    427   return Math.min(Math.max(number, min), max);
    428 };
    429