1/* Copyright (c) 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 Caret browsing content script, runs in each frame.
7 *
8 * The behavior is based on Mozilla's spec whenever possible:
9 *   http://www.mozilla.org/access/keyboard/proposal
10 *
11 * The one exception is that Esc is used to escape out of a form control,
12 * rather than their proposed key (which doesn't seem to work in the
13 * latest Firefox anyway).
14 *
15 * Some details about how Chrome selection works, which will help in
16 * understanding the code:
17 *
18 * The Selection object (window.getSelection()) has four components that
19 * completely describe the state of the caret or selection:
20 *
21 * base and anchor: this is the start of the selection, the fixed point.
22 * extent and focus: this is the end of the selection, the part that
23 *     moves when you hold down shift and press the left or right arrows.
24 *
25 * When the selection is a cursor, the base, anchor, extent, and focus are
26 * all the same.
27 *
28 * There's only one time when the base and anchor are not the same, or the
29 * extent and focus are not the same, and that's when the selection is in
30 * an ambiguous state - i.e. it's not clear which edge is the focus and which
31 * is the anchor. As an example, if you double-click to select a word, then
32 * the behavior is dependent on your next action. If you press Shift+Right,
33 * the right edge becomes the focus. But if you press Shift+Left, the left
34 * edge becomes the focus.
35 *
36 * When the selection is in an ambiguous state, the base and extent are set
37 * to the position where the mouse clicked, and the anchor and focus are set
38 * to the boundaries of the selection.
39 *
40 * The only way to set the selection and give it direction is to use
41 * the non-standard Selection.setBaseAndExtent method. If you try to use
42 * Selection.addRange(), the anchor will always be on the left and the focus
43 * will always be on the right, making it impossible to manipulate
44 * selections that move from right to left.
45 *
46 * Finally, Chrome will throw an exception if you try to set an invalid
47 * selection - a selection where the left and right edges are not the same,
48 * but it doesn't span any visible characters. A common example is that
49 * there are often many whitespace characters in the DOM that are not
50 * visible on the page; trying to select them will fail. Another example is
51 * any node that's invisible or not displayed.
52 *
53 * While there are probably many possible methods to determine what is
54 * selectable, this code uses the method of determining if there's a valid
55 * bounding box for the range or not - keep moving the cursor forwards until
56 * the range from the previous position and candidate next position has a
57 * valid bounding box.
58 */
59
60/**
61 * Return whether a node is focusable. This includes nodes whose tabindex
62 * attribute is set to "-1" explicitly - these nodes are not in the tab
63 * order, but they should still be focused if the user navigates to them
64 * using linear or smart DOM navigation.
65 *
66 * Note that when the tabIndex property of an Element is -1, that doesn't
67 * tell us whether the tabIndex attribute is missing or set to "-1" explicitly,
68 * so we have to check the attribute.
69 *
70 * @param {Object} targetNode The node to check if it's focusable.
71 * @return {boolean} True if the node is focusable.
72 */
73function isFocusable(targetNode) {
74  if (!targetNode || typeof(targetNode.tabIndex) != 'number') {
75    return false;
76  }
77
78  if (targetNode.tabIndex >= 0) {
79    return true;
80  }
81
82  if (targetNode.hasAttribute &&
83      targetNode.hasAttribute('tabindex') &&
84      targetNode.getAttribute('tabindex') == '-1') {
85    return true;
86  }
87
88  return false;
89}
90
91/**
92 * Determines whether or not a node is or is the descendant of another node.
93 *
94 * @param {Object} node The node to be checked.
95 * @param {Object} ancestor The node to see if it's a descendant of.
96 * @return {boolean} True if the node is ancestor or is a descendant of it.
97 */
98function isDescendantOfNode(node, ancestor) {
99  while (node && ancestor) {
100    if (node.isSameNode(ancestor)) {
101      return true;
102    }
103    node = node.parentNode;
104  }
105  return false;
106}
107
108
109
110/**
111 * The class handling the Caret Browsing implementation in the page.
112 * Installs a keydown listener that always responds to the F7 key,
113 * sets up communication with the background page, and then when caret
114 * browsing is enabled, response to various key events to move the caret
115 * or selection within the text content of the document. Uses the native
116 * Chrome selection wherever possible, but displays its own flashing
117 * caret using a DIV because there's no native caret available.
118 * @constructor
119 */
120var CaretBrowsing = function() {};
121
122/**
123 * Is caret browsing enabled?
124 * @type {boolean}
125 */
126CaretBrowsing.isEnabled = false;
127
128/**
129 * Keep it enabled even when flipped off (for the options page)?
130 * @type {boolean}
131 */
132CaretBrowsing.forceEnabled = false;
133
134/**
135 * What to do when the caret appears?
136 * @type {string}
137 */
138CaretBrowsing.onEnable;
139
140/**
141 * What to do when the caret jumps?
142 * @type {string}
143 */
144CaretBrowsing.onJump;
145
146/**
147 * Is this window / iframe focused? We won't show the caret if not,
148 * especially so that carets aren't shown in two iframes of the same
149 * tab.
150 * @type {boolean}
151 */
152CaretBrowsing.isWindowFocused = false;
153
154/**
155 * Is the caret actually visible? This is true only if isEnabled and
156 * isWindowFocused are both true.
157 * @type {boolean}
158 */
159CaretBrowsing.isCaretVisible = false;
160
161/**
162 * The actual caret element, an absolute-positioned flashing line.
163 * @type {Element}
164 */
165CaretBrowsing.caretElement;
166
167/**
168 * The x-position of the caret, in absolute pixels.
169 * @type {number}
170 */
171CaretBrowsing.caretX = 0;
172
173/**
174 * The y-position of the caret, in absolute pixels.
175 * @type {number}
176 */
177CaretBrowsing.caretY = 0;
178
179/**
180 * The width of the caret in pixels.
181 * @type {number}
182 */
183CaretBrowsing.caretWidth = 0;
184
185/**
186 * The height of the caret in pixels.
187 * @type {number}
188 */
189CaretBrowsing.caretHeight = 0;
190
191/**
192 * The foregroundc color.
193 * @type {string}
194 */
195CaretBrowsing.caretForeground = '#000';
196
197/**
198 * The backgroundc color.
199 * @type {string}
200 */
201CaretBrowsing.caretBackground = '#fff';
202
203/**
204 * Is the selection collapsed, i.e. are the start and end locations
205 * the same? If so, our blinking caret image is shown; otherwise
206 * the Chrome selection is shown.
207 * @type {boolean}
208 */
209CaretBrowsing.isSelectionCollapsed = false;
210
211/**
212 * The id returned by window.setInterval for our blink function, so
213 * we can cancel it when caret browsing is disabled.
214 * @type {number?}
215 */
216CaretBrowsing.blinkFunctionId = null;
217
218/**
219 * The desired x-coordinate to match when moving the caret up and down.
220 * To match the behavior as documented in Mozilla's caret browsing spec
221 * (http://www.mozilla.org/access/keyboard/proposal), we keep track of the
222 * initial x position when the user starts moving the caret up and down,
223 * so that the x position doesn't drift as you move throughout lines, but
224 * stays as close as possible to the initial position. This is reset when
225 * moving left or right or clicking.
226 * @type {number?}
227 */
228CaretBrowsing.targetX = null;
229
230/**
231 * A flag that flips on or off as the caret blinks.
232 * @type {boolean}
233 */
234CaretBrowsing.blinkFlag = true;
235
236/**
237 * Whether or not we're on a Mac - affects modifier keys.
238 * @type {boolean}
239 */
240CaretBrowsing.isMac = (navigator.appVersion.indexOf("Mac") != -1);
241
242/**
243 * Check if a node is a control that normally allows the user to interact
244 * with it using arrow keys. We won't override the arrow keys when such a
245 * control has focus, the user must press Escape to do caret browsing outside
246 * that control.
247 * @param {Node} node A node to check.
248 * @return {boolean} True if this node is a control that the user can
249 *     interact with using arrow keys.
250 */
251CaretBrowsing.isControlThatNeedsArrowKeys = function(node) {
252  if (!node) {
253    return false;
254  }
255
256  if (node == document.body || node != document.activeElement) {
257    return false;
258  }
259
260  if (node.constructor == HTMLSelectElement) {
261    return true;
262  }
263
264  if (node.constructor == HTMLInputElement) {
265    switch (node.type) {
266      case 'email':
267      case 'number':
268      case 'password':
269      case 'search':
270      case 'text':
271      case 'tel':
272      case 'url':
273      case '':
274        return true;  // All of these are text boxes.
275      case 'datetime':
276      case 'datetime-local':
277      case 'date':
278      case 'month':
279      case 'radio':
280      case 'range':
281      case 'week':
282        return true;  // These are other input elements that use arrows.
283    }
284  }
285
286  // Handle focusable ARIA controls.
287  if (node.getAttribute && isFocusable(node)) {
288    var role = node.getAttribute('role');
289    switch (role) {
290      case 'combobox':
291      case 'grid':
292      case 'gridcell':
293      case 'listbox':
294      case 'menu':
295      case 'menubar':
296      case 'menuitem':
297      case 'menuitemcheckbox':
298      case 'menuitemradio':
299      case 'option':
300      case 'radiogroup':
301      case 'scrollbar':
302      case 'slider':
303      case 'spinbutton':
304      case 'tab':
305      case 'tablist':
306      case 'textbox':
307      case 'tree':
308      case 'treegrid':
309      case 'treeitem':
310        return true;
311    }
312  }
313
314  return false;
315};
316
317/**
318 * If there's no initial selection, set the cursor just before the
319 * first text character in the document.
320 */
321CaretBrowsing.setInitialCursor = function() {
322  var sel = window.getSelection();
323  if (sel.rangeCount > 0) {
324    return;
325  }
326
327  var start = new Cursor(document.body, 0, '');
328  var end = new Cursor(document.body, 0, '');
329  var nodesCrossed = [];
330  var result = TraverseUtil.getNextChar(start, end, nodesCrossed, true);
331  if (result == null) {
332    return;
333  }
334  CaretBrowsing.setAndValidateSelection(start, start);
335};
336
337/**
338 * Set focus to a node if it's focusable. If it's an input element,
339 * select the text, otherwise it doesn't appear focused to the user.
340 * Every other control behaves normally if you just call focus() on it.
341 * @param {Node} node The node to focus.
342 * @return {boolean} True if the node was focused.
343 */
344CaretBrowsing.setFocusToNode = function(node) {
345  while (node && node != document.body) {
346    if (isFocusable(node) && node.constructor != HTMLIFrameElement) {
347      node.focus();
348      if (node.constructor == HTMLInputElement && node.select) {
349        node.select();
350      }
351      return true;
352    }
353    node = node.parentNode;
354  }
355
356  return false;
357};
358
359/**
360 * Set focus to the first focusable node in the given list.
361 * select the text, otherwise it doesn't appear focused to the user.
362 * Every other control behaves normally if you just call focus() on it.
363 * @param {Array.<Node>} nodeList An array of nodes to focus.
364 * @return {boolean} True if the node was focused.
365 */
366CaretBrowsing.setFocusToFirstFocusable = function(nodeList) {
367  for (var i = 0; i < nodeList.length; i++) {
368    if (CaretBrowsing.setFocusToNode(nodeList[i])) {
369      return true;
370    }
371  }
372  return false;
373};
374
375/**
376 * Set the caret element's normal style, i.e. not when animating.
377 */
378CaretBrowsing.setCaretElementNormalStyle = function() {
379  var element = CaretBrowsing.caretElement;
380  element.className = 'CaretBrowsing_Caret';
381  element.style.opacity = CaretBrowsing.isSelectionCollapsed ? '1.0' : '0.0';
382  element.style.left = CaretBrowsing.caretX + 'px';
383  element.style.top = CaretBrowsing.caretY + 'px';
384  element.style.width = CaretBrowsing.caretWidth + 'px';
385  element.style.height = CaretBrowsing.caretHeight + 'px';
386  element.style.color = CaretBrowsing.caretForeground;
387};
388
389/**
390 * Animate the caret element into the normal style.
391 */
392CaretBrowsing.animateCaretElement = function() {
393  var element = CaretBrowsing.caretElement;
394  element.style.left = (CaretBrowsing.caretX - 50) + 'px';
395  element.style.top = (CaretBrowsing.caretY - 100) + 'px';
396  element.style.width = (CaretBrowsing.caretWidth + 100) + 'px';
397  element.style.height = (CaretBrowsing.caretHeight + 200) + 'px';
398  element.className = 'CaretBrowsing_AnimateCaret';
399
400  // Start the animation. The setTimeout is so that the old values will get
401  // applied first, so we can animate to the new values.
402  window.setTimeout(function() {
403    if (!CaretBrowsing.caretElement) {
404      return;
405    }
406    CaretBrowsing.setCaretElementNormalStyle();
407    element.style['-webkit-transition'] = 'all 0.8s ease-in';
408    function listener() {
409      element.removeEventListener(
410          'webkitTransitionEnd', listener, false);
411      element.style['-webkit-transition'] = 'none';
412    }
413    element.addEventListener(
414        'webkitTransitionEnd', listener, false);
415  }, 0);
416};
417
418/**
419 * Quick flash and then show the normal caret style.
420 */
421CaretBrowsing.flashCaretElement = function() {
422  var x = CaretBrowsing.caretX - window.pageXOffset;
423  var y = CaretBrowsing.caretY - window.pageYOffset;
424  var height = CaretBrowsing.caretHeight;
425
426  var vert = document.createElement('div');
427  vert.className = 'CaretBrowsing_FlashVert';
428  vert.style.left = (x - 6) + 'px';
429  vert.style.top = (y - 100) + 'px';
430  vert.style.width = '11px';
431  vert.style.height = (200) + 'px';
432  document.body.appendChild(vert);
433
434  window.setTimeout(function() {
435    document.body.removeChild(vert);
436    if (CaretBrowsing.caretElement) {
437      CaretBrowsing.setCaretElementNormalStyle();
438    }
439  }, 250);
440};
441
442/**
443 * Create the caret element. This assumes that caretX, caretY,
444 * caretWidth, and caretHeight have all been set. The caret is
445 * animated in so the user can find it when it first appears.
446 */
447CaretBrowsing.createCaretElement = function() {
448  var element = document.createElement('div');
449  element.className = 'CaretBrowsing_Caret';
450  document.body.appendChild(element);
451  CaretBrowsing.caretElement = element;
452
453  if (CaretBrowsing.onEnable == 'anim') {
454    CaretBrowsing.animateCaretElement();
455  } else if (CaretBrowsing.onEnable == 'flash') {
456    CaretBrowsing.flashCaretElement();
457  } else {
458    CaretBrowsing.setCaretElementNormalStyle();
459  }
460};
461
462/**
463 * Recreate the caret element, triggering any intro animation.
464 */
465CaretBrowsing.recreateCaretElement = function() {
466  if (CaretBrowsing.caretElement) {
467    window.clearInterval(CaretBrowsing.blinkFunctionId);
468    CaretBrowsing.caretElement.parentElement.removeChild(
469        CaretBrowsing.caretElement);
470    CaretBrowsing.caretElement = null;
471    CaretBrowsing.updateIsCaretVisible();
472  }
473};
474
475/**
476 * Get the rectangle for a cursor position. This is tricky because
477 * you can't get the bounding rectangle of an empty range, so this function
478 * computes the rect by trying a range including one character earlier or
479 * later than the cursor position.
480 * @param {Cursor} cursor A single cursor position.
481 * @return {{left: number, top: number, width: number, height: number}}
482 *     The bounding rectangle of the cursor.
483 */
484CaretBrowsing.getCursorRect = function(cursor) {
485  var node = cursor.node;
486  var index = cursor.index;
487  var rect = {
488    left: 0,
489    top: 0,
490    width: 1,
491    height: 0
492  };
493  if (node.constructor == Text) {
494    var left = index;
495    var right = index;
496    var max = node.data.length;
497    var newRange = document.createRange();
498    while (left > 0 || right < max) {
499      if (left > 0) {
500        left--;
501        newRange.setStart(node, left);
502        newRange.setEnd(node, index);
503        var rangeRect = newRange.getBoundingClientRect();
504        if (rangeRect && rangeRect.width && rangeRect.height) {
505          rect.left = rangeRect.right;
506          rect.top = rangeRect.top;
507          rect.height = rangeRect.height;
508          break;
509        }
510      }
511      if (right < max) {
512        right++;
513        newRange.setStart(node, index);
514        newRange.setEnd(node, right);
515        var rangeRect = newRange.getBoundingClientRect();
516        if (rangeRect && rangeRect.width && rangeRect.height) {
517          rect.left = rangeRect.left;
518          rect.top = rangeRect.top;
519          rect.height = rangeRect.height;
520          break;
521        }
522      }
523    }
524  } else {
525    rect.height = node.offsetHeight;
526    while (node !== null) {
527      rect.left += node.offsetLeft;
528      rect.top += node.offsetTop;
529      node = node.offsetParent;
530    }
531  }
532  rect.left += window.pageXOffset;
533  rect.top += window.pageYOffset;
534  return rect;
535};
536
537/**
538 * Compute the new location of the caret or selection and update
539 * the element as needed.
540 * @param {boolean} scrollToSelection If true, will also scroll the page
541 *     to the caret / selection location.
542 */
543CaretBrowsing.updateCaretOrSelection = function(scrollToSelection) {
544  var previousX = CaretBrowsing.caretX;
545  var previousY = CaretBrowsing.caretY;
546
547  var sel = window.getSelection();
548  if (sel.rangeCount == 0) {
549    if (CaretBrowsing.caretElement) {
550      CaretBrowsing.isSelectionCollapsed = false;
551      CaretBrowsing.caretElement.style.opacity = '0.0';
552    }
553    return;
554  }
555
556  var range = sel.getRangeAt(0);
557  if (!range) {
558    if (CaretBrowsing.caretElement) {
559      CaretBrowsing.isSelectionCollapsed = false;
560      CaretBrowsing.caretElement.style.opacity = '0.0';
561    }
562    return;
563  }
564
565  if (CaretBrowsing.isControlThatNeedsArrowKeys(document.activeElement)) {
566    var node = document.activeElement;
567    CaretBrowsing.caretWidth = node.offsetWidth;
568    CaretBrowsing.caretHeight = node.offsetHeight;
569    CaretBrowsing.caretX = 0;
570    CaretBrowsing.caretY = 0;
571    while (node.offsetParent) {
572      CaretBrowsing.caretX += node.offsetLeft;
573      CaretBrowsing.caretY += node.offsetTop;
574      node = node.offsetParent;
575    }
576    CaretBrowsing.isSelectionCollapsed = false;
577  } else if (range.startOffset != range.endOffset ||
578             range.startContainer != range.endContainer) {
579    var rect = range.getBoundingClientRect();
580    if (!rect) {
581      return;
582    }
583    CaretBrowsing.caretX = rect.left + window.pageXOffset;
584    CaretBrowsing.caretY = rect.top + window.pageYOffset;
585    CaretBrowsing.caretWidth = rect.width;
586    CaretBrowsing.caretHeight = rect.height;
587    CaretBrowsing.isSelectionCollapsed = false;
588  } else {
589    var rect = CaretBrowsing.getCursorRect(
590        new Cursor(range.startContainer,
591                   range.startOffset,
592                   TraverseUtil.getNodeText(range.startContainer)));
593    CaretBrowsing.caretX = rect.left;
594    CaretBrowsing.caretY = rect.top;
595    CaretBrowsing.caretWidth = rect.width;
596    CaretBrowsing.caretHeight = rect.height;
597    CaretBrowsing.isSelectionCollapsed = true;
598  }
599
600  if (!CaretBrowsing.caretElement) {
601    CaretBrowsing.createCaretElement();
602  } else {
603    var element = CaretBrowsing.caretElement;
604    if (CaretBrowsing.isSelectionCollapsed) {
605      element.style.opacity = '1.0';
606      element.style.left = CaretBrowsing.caretX + 'px';
607      element.style.top = CaretBrowsing.caretY + 'px';
608      element.style.width = CaretBrowsing.caretWidth + 'px';
609      element.style.height = CaretBrowsing.caretHeight + 'px';
610    } else {
611      element.style.opacity = '0.0';
612    }
613  }
614
615  var elem = range.startContainer;
616  if (elem.constructor == Text)
617    elem = elem.parentElement;
618  var style = window.getComputedStyle(elem);
619  var bg = axs.utils.getBgColor(style, elem);
620  var fg = axs.utils.getFgColor(style, elem, bg);
621  CaretBrowsing.caretBackground = axs.utils.colorToString(bg);
622  CaretBrowsing.caretForeground = axs.utils.colorToString(fg);
623
624  if (scrollToSelection) {
625    // Scroll just to the "focus" position of the selection,
626    // the part the user is manipulating.
627    var rect = CaretBrowsing.getCursorRect(
628        new Cursor(sel.focusNode, sel.focusOffset,
629                   TraverseUtil.getNodeText(sel.focusNode)));
630
631    var yscroll = window.pageYOffset;
632    var pageHeight = window.innerHeight;
633    var caretY = rect.top;
634    var caretHeight = Math.min(rect.height, 30);
635    if (yscroll + pageHeight < caretY + caretHeight) {
636      window.scroll(0, (caretY + caretHeight - pageHeight + 100));
637    } else if (caretY < yscroll) {
638      window.scroll(0, (caretY - 100));
639    }
640  }
641
642  if (Math.abs(previousX - CaretBrowsing.caretX) > 500 ||
643      Math.abs(previousY - CaretBrowsing.caretY) > 100) {
644    if (CaretBrowsing.onJump == 'anim') {
645      CaretBrowsing.animateCaretElement();
646    } else if (CaretBrowsing.onJump == 'flash') {
647      CaretBrowsing.flashCaretElement();
648    }
649  }
650};
651
652/**
653 * Return true if the selection directionality is ambiguous, which happens
654 * if, for example, the user double-clicks in the middle of a word to select
655 * it. In that case, the selection should extend by the right edge if the
656 * user presses right, and by the left edge if the user presses left.
657 * @param {Selection} sel The selection.
658 * @return {boolean} True if the selection directionality is ambiguous.
659 */
660CaretBrowsing.isAmbiguous = function(sel) {
661  return (sel.anchorNode != sel.baseNode ||
662          sel.anchorOffset != sel.baseOffset ||
663          sel.focusNode != sel.extentNode ||
664          sel.focusOffset != sel.extentOffset);
665};
666
667/**
668 * Create a Cursor from the anchor position of the selection, the
669 * part that doesn't normally move.
670 * @param {Selection} sel The selection.
671 * @return {Cursor} A cursor pointing to the selection's anchor location.
672 */
673CaretBrowsing.makeAnchorCursor = function(sel) {
674  return new Cursor(sel.anchorNode, sel.anchorOffset,
675                    TraverseUtil.getNodeText(sel.anchorNode));
676};
677
678/**
679 * Create a Cursor from the focus position of the selection.
680 * @param {Selection} sel The selection.
681 * @return {Cursor} A cursor pointing to the selection's focus location.
682 */
683CaretBrowsing.makeFocusCursor = function(sel) {
684  return new Cursor(sel.focusNode, sel.focusOffset,
685                    TraverseUtil.getNodeText(sel.focusNode));
686};
687
688/**
689 * Create a Cursor from the left boundary of the selection - the boundary
690 * closer to the start of the document.
691 * @param {Selection} sel The selection.
692 * @return {Cursor} A cursor pointing to the selection's left boundary.
693 */
694CaretBrowsing.makeLeftCursor = function(sel) {
695  var range = sel.rangeCount == 1 ? sel.getRangeAt(0) : null;
696  if (range &&
697      range.endContainer == sel.anchorNode &&
698      range.endOffset == sel.anchorOffset) {
699    return CaretBrowsing.makeFocusCursor(sel);
700  } else {
701    return CaretBrowsing.makeAnchorCursor(sel);
702  }
703};
704
705/**
706 * Create a Cursor from the right boundary of the selection - the boundary
707 * closer to the end of the document.
708 * @param {Selection} sel The selection.
709 * @return {Cursor} A cursor pointing to the selection's right boundary.
710 */
711CaretBrowsing.makeRightCursor = function(sel) {
712  var range = sel.rangeCount == 1 ? sel.getRangeAt(0) : null;
713  if (range &&
714      range.endContainer == sel.anchorNode &&
715      range.endOffset == sel.anchorOffset) {
716    return CaretBrowsing.makeAnchorCursor(sel);
717  } else {
718    return CaretBrowsing.makeFocusCursor(sel);
719  }
720};
721
722/**
723 * Try to set the window's selection to be between the given start and end
724 * cursors, and return whether or not it was successful.
725 * @param {Cursor} start The start position.
726 * @param {Cursor} end The end position.
727 * @return {boolean} True if the selection was successfully set.
728 */
729CaretBrowsing.setAndValidateSelection = function(start, end) {
730  var sel = window.getSelection();
731  sel.setBaseAndExtent(start.node, start.index, end.node, end.index);
732
733  if (sel.rangeCount != 1) {
734    return false;
735  }
736
737  return (sel.anchorNode == start.node &&
738          sel.anchorOffset == start.index &&
739          sel.focusNode == end.node &&
740          sel.focusOffset == end.index);
741};
742
743/**
744 * Note: the built-in function by the same name is unreliable.
745 * @param {Selection} sel The selection.
746 * @return {boolean} True if the start and end positions are the same.
747 */
748CaretBrowsing.isCollapsed = function(sel) {
749  return (sel.anchorOffset == sel.focusOffset &&
750          sel.anchorNode == sel.focusNode);
751};
752
753/**
754 * Determines if the modifier key is held down that should cause
755 * the cursor to move by word rather than by character.
756 * @param {Event} evt A keyboard event.
757 * @return {boolean} True if the cursor should move by word.
758 */
759CaretBrowsing.isMoveByWordEvent = function(evt) {
760  if (CaretBrowsing.isMac) {
761    return evt.altKey;
762  } else {
763    return evt.ctrlKey;
764  }
765};
766
767/**
768 * Moves the cursor forwards to the next valid position.
769 * @param {Cursor} cursor The current cursor location.
770 *     On exit, the cursor will be at the next position.
771 * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the
772 *     initial and final cursor position will be pushed onto this array.
773 * @return {?string} The character reached, or null if the bottom of the
774 *     document has been reached.
775 */
776CaretBrowsing.forwards = function(cursor, nodesCrossed) {
777  var previousCursor = cursor.clone();
778  var result = TraverseUtil.forwardsChar(cursor, nodesCrossed);
779
780  // Work around the fact that TraverseUtil.forwardsChar returns once per
781  // char in a block of text, rather than once per possible selection
782  // position in a block of text.
783  if (result && cursor.node != previousCursor.node && cursor.index > 0) {
784    cursor.index = 0;
785  }
786
787  return result;
788};
789
790/**
791 * Moves the cursor backwards to the previous valid position.
792 * @param {Cursor} cursor The current cursor location.
793 *     On exit, the cursor will be at the previous position.
794 * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the
795 *     initial and final cursor position will be pushed onto this array.
796 * @return {?string} The character reached, or null if the top of the
797 *     document has been reached.
798 */
799CaretBrowsing.backwards = function(cursor, nodesCrossed) {
800  var previousCursor = cursor.clone();
801  var result = TraverseUtil.backwardsChar(cursor, nodesCrossed);
802
803  // Work around the fact that TraverseUtil.backwardsChar returns once per
804  // char in a block of text, rather than once per possible selection
805  // position in a block of text.
806  if (result &&
807      cursor.node != previousCursor.node &&
808      cursor.index < cursor.text.length) {
809    cursor.index = cursor.text.length;
810  }
811
812  return result;
813};
814
815/**
816 * Called when the user presses the right arrow. If there's a selection,
817 * moves the cursor to the end of the selection range. If it's a cursor,
818 * moves past one character.
819 * @param {Event} evt The DOM event.
820 * @return {boolean} True if the default action should be performed.
821 */
822CaretBrowsing.moveRight = function(evt) {
823  CaretBrowsing.targetX = null;
824
825  var sel = window.getSelection();
826  if (!evt.shiftKey && !CaretBrowsing.isCollapsed(sel)) {
827    var right = CaretBrowsing.makeRightCursor(sel);
828    CaretBrowsing.setAndValidateSelection(right, right);
829    return false;
830  }
831
832  var start = CaretBrowsing.isAmbiguous(sel) ?
833              CaretBrowsing.makeLeftCursor(sel) :
834              CaretBrowsing.makeAnchorCursor(sel);
835  var end = CaretBrowsing.isAmbiguous(sel) ?
836            CaretBrowsing.makeRightCursor(sel) :
837            CaretBrowsing.makeFocusCursor(sel);
838  var previousEnd = end.clone();
839  var nodesCrossed = [];
840  while (true) {
841    var result;
842    if (CaretBrowsing.isMoveByWordEvent(evt)) {
843      result = TraverseUtil.getNextWord(previousEnd, end, nodesCrossed);
844    } else {
845      previousEnd = end.clone();
846      result = CaretBrowsing.forwards(end, nodesCrossed);
847    }
848
849    if (result === null) {
850      return CaretBrowsing.moveLeft(evt);
851    }
852
853    if (CaretBrowsing.setAndValidateSelection(
854            evt.shiftKey ? start : end, end)) {
855      break;
856    }
857  }
858
859  if (!evt.shiftKey) {
860    nodesCrossed.push(end.node);
861    CaretBrowsing.setFocusToFirstFocusable(nodesCrossed);
862  }
863
864  return false;
865};
866
867/**
868 * Called when the user presses the left arrow. If there's a selection,
869 * moves the cursor to the start of the selection range. If it's a cursor,
870 * moves backwards past one character.
871 * @param {Event} evt The DOM event.
872 * @return {boolean} True if the default action should be performed.
873 */
874CaretBrowsing.moveLeft = function(evt) {
875  CaretBrowsing.targetX = null;
876
877  var sel = window.getSelection();
878  if (!evt.shiftKey && !CaretBrowsing.isCollapsed(sel)) {
879    var left = CaretBrowsing.makeLeftCursor(sel);
880    CaretBrowsing.setAndValidateSelection(left, left);
881    return false;
882  }
883
884  var start = CaretBrowsing.isAmbiguous(sel) ?
885              CaretBrowsing.makeLeftCursor(sel) :
886              CaretBrowsing.makeFocusCursor(sel);
887  var end = CaretBrowsing.isAmbiguous(sel) ?
888            CaretBrowsing.makeRightCursor(sel) :
889            CaretBrowsing.makeAnchorCursor(sel);
890  var previousStart = start.clone();
891  var nodesCrossed = [];
892  while (true) {
893    var result;
894    if (CaretBrowsing.isMoveByWordEvent(evt)) {
895      result = TraverseUtil.getPreviousWord(
896          start, previousStart, nodesCrossed);
897    } else {
898      previousStart = start.clone();
899      result = CaretBrowsing.backwards(start, nodesCrossed);
900    }
901
902    if (result === null) {
903      break;
904    }
905
906    if (CaretBrowsing.setAndValidateSelection(
907            evt.shiftKey ? end : start, start)) {
908      break;
909    }
910  }
911
912  if (!evt.shiftKey) {
913    nodesCrossed.push(start.node);
914    CaretBrowsing.setFocusToFirstFocusable(nodesCrossed);
915  }
916
917  return false;
918};
919
920
921/**
922 * Called when the user presses the down arrow. If there's a selection,
923 * moves the cursor to the end of the selection range. If it's a cursor,
924 * attempts to move to the equivalent horizontal pixel position in the
925 * subsequent line of text. If this is impossible, go to the first character
926 * of the next line.
927 * @param {Event} evt The DOM event.
928 * @return {boolean} True if the default action should be performed.
929 */
930CaretBrowsing.moveDown = function(evt) {
931  var sel = window.getSelection();
932  if (!evt.shiftKey && !CaretBrowsing.isCollapsed(sel)) {
933    var right = CaretBrowsing.makeRightCursor(sel);
934    CaretBrowsing.setAndValidateSelection(right, right);
935    return false;
936  }
937
938  var start = CaretBrowsing.isAmbiguous(sel) ?
939              CaretBrowsing.makeLeftCursor(sel) :
940              CaretBrowsing.makeAnchorCursor(sel);
941  var end = CaretBrowsing.isAmbiguous(sel) ?
942            CaretBrowsing.makeRightCursor(sel) :
943            CaretBrowsing.makeFocusCursor(sel);
944  var endRect = CaretBrowsing.getCursorRect(end);
945  if (CaretBrowsing.targetX === null) {
946    CaretBrowsing.targetX = endRect.left;
947  }
948  var previousEnd = end.clone();
949  var leftPos = end.clone();
950  var rightPos = end.clone();
951  var bestPos = null;
952  var bestY = null;
953  var bestDelta = null;
954  var bestHeight = null;
955  var nodesCrossed = [];
956  var y = -1;
957  while (true) {
958    if (null === CaretBrowsing.forwards(rightPos, nodesCrossed)) {
959      if (CaretBrowsing.setAndValidateSelection(
960            evt.shiftKey ? start : leftPos, leftPos)) {
961        break;
962      } else {
963        return CaretBrowsing.moveLeft(evt);
964      }
965      break;
966    }
967    var range = document.createRange();
968    range.setStart(leftPos.node, leftPos.index);
969    range.setEnd(rightPos.node, rightPos.index);
970    var rect = range.getBoundingClientRect();
971    if (rect && rect.width < rect.height) {
972      y = rect.top + window.pageYOffset;
973
974      // Return the best match so far if we get half a line past the best.
975      if (bestY != null && y > bestY + bestHeight / 2) {
976        if (CaretBrowsing.setAndValidateSelection(
977                evt.shiftKey ? start : bestPos, bestPos)) {
978          break;
979        } else {
980          bestY = null;
981        }
982      }
983
984      // Stop here if we're an entire line the wrong direction
985      // (for example, we reached the top of the next column).
986      if (y < endRect.top - endRect.height) {
987        if (CaretBrowsing.setAndValidateSelection(
988                evt.shiftKey ? start : leftPos, leftPos)) {
989          break;
990        }
991      }
992
993      // Otherwise look to see if this current position is on the
994      // next line and better than the previous best match, if any.
995      if (y >= endRect.top + endRect.height) {
996        var deltaLeft = Math.abs(CaretBrowsing.targetX - rect.left);
997        if ((bestDelta == null || deltaLeft < bestDelta) &&
998            (leftPos.node != end.node || leftPos.index != end.index)) {
999          bestPos = leftPos.clone();
1000          bestY = y;
1001          bestDelta = deltaLeft;
1002          bestHeight = rect.height;
1003        }
1004        var deltaRight = Math.abs(CaretBrowsing.targetX - rect.right);
1005        if (bestDelta == null || deltaRight < bestDelta) {
1006          bestPos = rightPos.clone();
1007          bestY = y;
1008          bestDelta = deltaRight;
1009          bestHeight = rect.height;
1010        }
1011
1012        // Return the best match so far if the deltas are getting worse,
1013        // not better.
1014        if (bestDelta != null &&
1015            deltaLeft > bestDelta &&
1016            deltaRight > bestDelta) {
1017          if (CaretBrowsing.setAndValidateSelection(
1018                  evt.shiftKey ? start : bestPos, bestPos)) {
1019            break;
1020          } else {
1021            bestY = null;
1022          }
1023        }
1024      }
1025    }
1026    leftPos = rightPos.clone();
1027  }
1028
1029  if (!evt.shiftKey) {
1030    CaretBrowsing.setFocusToNode(leftPos.node);
1031  }
1032
1033  return false;
1034};
1035
1036/**
1037 * Called when the user presses the up arrow. If there's a selection,
1038 * moves the cursor to the start of the selection range. If it's a cursor,
1039 * attempts to move to the equivalent horizontal pixel position in the
1040 * previous line of text. If this is impossible, go to the last character
1041 * of the previous line.
1042 * @param {Event} evt The DOM event.
1043 * @return {boolean} True if the default action should be performed.
1044 */
1045CaretBrowsing.moveUp = function(evt) {
1046  var sel = window.getSelection();
1047  if (!evt.shiftKey && !CaretBrowsing.isCollapsed(sel)) {
1048    var left = CaretBrowsing.makeLeftCursor(sel);
1049    CaretBrowsing.setAndValidateSelection(left, left);
1050    return false;
1051  }
1052
1053  var start = CaretBrowsing.isAmbiguous(sel) ?
1054              CaretBrowsing.makeLeftCursor(sel) :
1055              CaretBrowsing.makeFocusCursor(sel);
1056  var end = CaretBrowsing.isAmbiguous(sel) ?
1057            CaretBrowsing.makeRightCursor(sel) :
1058            CaretBrowsing.makeAnchorCursor(sel);
1059  var startRect = CaretBrowsing.getCursorRect(start);
1060  if (CaretBrowsing.targetX === null) {
1061    CaretBrowsing.targetX = startRect.left;
1062  }
1063  var previousStart = start.clone();
1064  var leftPos = start.clone();
1065  var rightPos = start.clone();
1066  var bestPos = null;
1067  var bestY = null;
1068  var bestDelta = null;
1069  var bestHeight = null;
1070  var nodesCrossed = [];
1071  var y = 999999;
1072  while (true) {
1073    if (null === CaretBrowsing.backwards(leftPos, nodesCrossed)) {
1074      CaretBrowsing.setAndValidateSelection(
1075          evt.shiftKey ? end : rightPos, rightPos);
1076      break;
1077    }
1078    var range = document.createRange();
1079    range.setStart(leftPos.node, leftPos.index);
1080    range.setEnd(rightPos.node, rightPos.index);
1081    var rect = range.getBoundingClientRect();
1082    if (rect && rect.width < rect.height) {
1083      y = rect.top + window.pageYOffset;
1084
1085      // Return the best match so far if we get half a line past the best.
1086      if (bestY != null && y < bestY - bestHeight / 2) {
1087        if (CaretBrowsing.setAndValidateSelection(
1088                evt.shiftKey ? end : bestPos, bestPos)) {
1089          break;
1090        } else {
1091          bestY = null;
1092        }
1093      }
1094
1095      // Exit if we're an entire line the wrong direction
1096      // (for example, we reached the bottom of the previous column.)
1097      if (y > startRect.top + startRect.height) {
1098        if (CaretBrowsing.setAndValidateSelection(
1099                evt.shiftKey ? end : rightPos, rightPos)) {
1100          break;
1101        }
1102      }
1103
1104      // Otherwise look to see if this current position is on the
1105      // next line and better than the previous best match, if any.
1106      if (y <= startRect.top - startRect.height) {
1107        var deltaLeft = Math.abs(CaretBrowsing.targetX - rect.left);
1108        if (bestDelta == null || deltaLeft < bestDelta) {
1109          bestPos = leftPos.clone();
1110          bestY = y;
1111          bestDelta = deltaLeft;
1112          bestHeight = rect.height;
1113        }
1114        var deltaRight = Math.abs(CaretBrowsing.targetX - rect.right);
1115        if ((bestDelta == null || deltaRight < bestDelta) &&
1116            (rightPos.node != start.node || rightPos.index != start.index)) {
1117          bestPos = rightPos.clone();
1118          bestY = y;
1119          bestDelta = deltaRight;
1120          bestHeight = rect.height;
1121        }
1122
1123        // Return the best match so far if the deltas are getting worse,
1124        // not better.
1125        if (bestDelta != null &&
1126            deltaLeft > bestDelta &&
1127            deltaRight > bestDelta) {
1128          if (CaretBrowsing.setAndValidateSelection(
1129                  evt.shiftKey ? end : bestPos, bestPos)) {
1130            break;
1131          } else {
1132            bestY = null;
1133          }
1134        }
1135      }
1136    }
1137    rightPos = leftPos.clone();
1138  }
1139
1140  if (!evt.shiftKey) {
1141    CaretBrowsing.setFocusToNode(rightPos.node);
1142  }
1143
1144  return false;
1145};
1146
1147/**
1148 * Set the document's selection to surround a control, so that the next
1149 * arrow key they press will allow them to explore the content before
1150 * or after a given control.
1151 * @param {Node} control The control to escape from.
1152 */
1153CaretBrowsing.escapeFromControl = function(control) {
1154  control.blur();
1155
1156  var start = new Cursor(control, 0, '');
1157  var previousStart = start.clone();
1158  var end = new Cursor(control, 0, '');
1159  var previousEnd = end.clone();
1160
1161  var nodesCrossed = [];
1162  while (true) {
1163    if (null === CaretBrowsing.backwards(start, nodesCrossed)) {
1164      break;
1165    }
1166
1167    var r = document.createRange();
1168    r.setStart(start.node, start.index);
1169    r.setEnd(previousStart.node, previousStart.index);
1170    if (r.getBoundingClientRect()) {
1171      break;
1172    }
1173    previousStart = start.clone();
1174  }
1175  while (true) {
1176    if (null === CaretBrowsing.forwards(end, nodesCrossed)) {
1177      break;
1178    }
1179    if (isDescendantOfNode(end.node, control)) {
1180      previousEnd = end.clone();
1181      continue;
1182    }
1183
1184    var r = document.createRange();
1185    r.setStart(previousEnd.node, previousEnd.index);
1186    r.setEnd(end.node, end.index);
1187    if (r.getBoundingClientRect()) {
1188      break;
1189    }
1190  }
1191
1192  if (!isDescendantOfNode(previousStart.node, control)) {
1193    start = previousStart.clone();
1194  }
1195
1196  if (!isDescendantOfNode(previousEnd.node, control)) {
1197    end = previousEnd.clone();
1198  }
1199
1200  CaretBrowsing.setAndValidateSelection(start, end);
1201
1202  window.setTimeout(function() {
1203    CaretBrowsing.updateCaretOrSelection(true);
1204  }, 0);
1205};
1206
1207/**
1208 * Toggle whether caret browsing is enabled or not.
1209 */
1210CaretBrowsing.toggle = function() {
1211  if (CaretBrowsing.forceEnabled) {
1212    CaretBrowsing.recreateCaretElement();
1213    return;
1214  }
1215
1216  CaretBrowsing.isEnabled = !CaretBrowsing.isEnabled;
1217  var obj = {};
1218  obj['enabled'] = CaretBrowsing.isEnabled;
1219  chrome.storage.sync.set(obj);
1220  CaretBrowsing.updateIsCaretVisible();
1221};
1222
1223/**
1224 * Event handler, called when a key is pressed.
1225 * @param {Event} evt The DOM event.
1226 * @return {boolean} True if the default action should be performed.
1227 */
1228CaretBrowsing.onKeyDown = function(evt) {
1229  if (evt.defaultPrevented) {
1230    return;
1231  }
1232
1233  if (evt.keyCode == 118) {  // F7
1234    CaretBrowsing.toggle();
1235  }
1236
1237  if (!CaretBrowsing.isEnabled) {
1238    return true;
1239  }
1240
1241  if (evt.target && CaretBrowsing.isControlThatNeedsArrowKeys(
1242      /** @type (Node) */(evt.target))) {
1243    if (evt.keyCode == 27) {
1244      CaretBrowsing.escapeFromControl(/** @type {Node} */(evt.target));
1245      evt.preventDefault();
1246      evt.stopPropagation();
1247      return false;
1248    } else {
1249      return true;
1250    }
1251  }
1252
1253  // If the current selection doesn't have a range, try to escape out of
1254  // the current control. If that fails, return so we don't fail whe
1255  // trying to move the cursor or selection.
1256  var sel = window.getSelection();
1257  if (sel.rangeCount == 0) {
1258    if (document.activeElement) {
1259      CaretBrowsing.escapeFromControl(document.activeElement);
1260      sel = window.getSelection();
1261    }
1262
1263    if (sel.rangeCount == 0) {
1264      return true;
1265    }
1266  }
1267
1268  if (CaretBrowsing.caretElement) {
1269    CaretBrowsing.caretElement.style.visibility = 'visible';
1270    CaretBrowsing.blinkFlag = true;
1271  }
1272
1273  var result = true;
1274  switch (evt.keyCode) {
1275    case 37:
1276      result = CaretBrowsing.moveLeft(evt);
1277      break;
1278    case 38:
1279      result = CaretBrowsing.moveUp(evt);
1280      break;
1281    case 39:
1282      result = CaretBrowsing.moveRight(evt);
1283      break;
1284    case 40:
1285      result = CaretBrowsing.moveDown(evt);
1286      break;
1287  }
1288
1289  if (result == false) {
1290    evt.preventDefault();
1291    evt.stopPropagation();
1292  }
1293
1294  window.setTimeout(function() {
1295    CaretBrowsing.updateCaretOrSelection(result == false);
1296  }, 0);
1297
1298  return result;
1299};
1300
1301/**
1302 * Event handler, called when the mouse is clicked. Chrome already
1303 * sets the selection when the mouse is clicked, all we need to do is
1304 * update our cursor.
1305 * @param {Event} evt The DOM event.
1306 * @return {boolean} True if the default action should be performed.
1307 */
1308CaretBrowsing.onClick = function(evt) {
1309  if (!CaretBrowsing.isEnabled) {
1310    return true;
1311  }
1312  window.setTimeout(function() {
1313    CaretBrowsing.targetX = null;
1314    CaretBrowsing.updateCaretOrSelection(false);
1315  }, 0);
1316  return true;
1317};
1318
1319/**
1320 * Called at a regular interval. Blink the cursor by changing its visibility.
1321 */
1322CaretBrowsing.caretBlinkFunction = function() {
1323  if (CaretBrowsing.caretElement) {
1324    if (CaretBrowsing.blinkFlag) {
1325      CaretBrowsing.caretElement.style.backgroundColor =
1326          CaretBrowsing.caretForeground;
1327      CaretBrowsing.blinkFlag = false;
1328    } else {
1329      CaretBrowsing.caretElement.style.backgroundColor =
1330          CaretBrowsing.caretBackground;
1331      CaretBrowsing.blinkFlag = true;
1332    }
1333  }
1334};
1335
1336/**
1337 * Update whether or not the caret is visible, based on whether caret browsing
1338 * is enabled and whether this window / iframe has focus.
1339 */
1340CaretBrowsing.updateIsCaretVisible = function() {
1341  CaretBrowsing.isCaretVisible =
1342      (CaretBrowsing.isEnabled && CaretBrowsing.isWindowFocused);
1343  if (CaretBrowsing.isCaretVisible && !CaretBrowsing.caretElement) {
1344    CaretBrowsing.setInitialCursor();
1345    CaretBrowsing.updateCaretOrSelection(true);
1346    if (CaretBrowsing.caretElement) {
1347      CaretBrowsing.blinkFunctionId = window.setInterval(
1348          CaretBrowsing.caretBlinkFunction, 500);
1349    }
1350  } else if (!CaretBrowsing.isCaretVisible &&
1351             CaretBrowsing.caretElement) {
1352    window.clearInterval(CaretBrowsing.blinkFunctionId);
1353    if (CaretBrowsing.caretElement) {
1354      CaretBrowsing.isSelectionCollapsed = false;
1355      CaretBrowsing.caretElement.parentElement.removeChild(
1356          CaretBrowsing.caretElement);
1357      CaretBrowsing.caretElement = null;
1358    }
1359  }
1360};
1361
1362/**
1363 * Called when the prefs get updated.
1364 */
1365CaretBrowsing.onPrefsUpdated = function() {
1366  chrome.storage.sync.get(null, function(result) {
1367    if (!CaretBrowsing.forceEnabled) {
1368      CaretBrowsing.isEnabled = result['enabled'];
1369    }
1370    CaretBrowsing.onEnable = result['onenable'];
1371    CaretBrowsing.onJump = result['onjump'];
1372    CaretBrowsing.recreateCaretElement();
1373  });
1374};
1375
1376/**
1377 * Called when this window / iframe gains focus.
1378 */
1379CaretBrowsing.onWindowFocus = function() {
1380  CaretBrowsing.isWindowFocused = true;
1381  CaretBrowsing.updateIsCaretVisible();
1382};
1383
1384/**
1385 * Called when this window / iframe loses focus.
1386 */
1387CaretBrowsing.onWindowBlur = function() {
1388  CaretBrowsing.isWindowFocused = false;
1389  CaretBrowsing.updateIsCaretVisible();
1390};
1391
1392/**
1393 * Initializes caret browsing by adding event listeners and extension
1394 * message listeners.
1395 */
1396CaretBrowsing.init = function() {
1397  CaretBrowsing.isWindowFocused = document.hasFocus();
1398
1399  document.addEventListener('keydown', CaretBrowsing.onKeyDown, false);
1400  document.addEventListener('click', CaretBrowsing.onClick, false);
1401  window.addEventListener('focus', CaretBrowsing.onWindowFocus, false);
1402  window.addEventListener('blur', CaretBrowsing.onWindowBlur, false);
1403};
1404
1405window.setTimeout(function() {
1406
1407  // Make sure the script only loads once.
1408  if (!window['caretBrowsingLoaded']) {
1409    window['caretBrowsingLoaded'] = true;
1410    CaretBrowsing.init();
1411
1412    if (document.body.getAttribute('caretbrowsing') == 'on') {
1413      CaretBrowsing.forceEnabled = true;
1414      CaretBrowsing.isEnabled = true;
1415      CaretBrowsing.updateIsCaretVisible();
1416    }
1417
1418    chrome.storage.onChanged.addListener(function() {
1419      CaretBrowsing.onPrefsUpdated();
1420    });
1421    CaretBrowsing.onPrefsUpdated();
1422  }
1423
1424}, 0);
1425