Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / chromeos / chromevox / chromevox / injected / ui / search_widget.js
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 JavaScript for poppup up a search widget and performing
7  * search within a page.
8  */
9
10 goog.provide('cvox.SearchWidget');
11
12 goog.require('cvox.AbstractEarcons');
13 goog.require('cvox.ApiImplementation');
14 goog.require('cvox.ChromeVox');
15 goog.require('cvox.CursorSelection');
16 goog.require('cvox.NavigationManager');
17 goog.require('cvox.Widget');
18
19
20 /**
21  * Initializes the search widget.
22  * @constructor
23  * @extends {cvox.Widget}
24  */
25 cvox.SearchWidget = function() {
26   /**
27    * @type {Element}
28    * @private
29    */
30   this.containerNode_ = null;
31
32   /**
33    * @type {Element}
34    * @private
35    */
36   this.txtNode_ = null;
37
38   /**
39    * @type {string}
40    * @const
41    * @private
42    */
43   this.PROMPT_ = 'Search:';
44
45   /**
46    * @type {boolean}
47    * @private
48    */
49   this.caseSensitive_ = false;
50
51   /**
52    * @type {boolean}
53    * @private
54   */
55   this.hasMatch_ = false;
56   goog.base(this);
57 };
58 goog.inherits(cvox.SearchWidget, cvox.Widget);
59 goog.addSingletonGetter(cvox.SearchWidget);
60
61
62 /**
63  * @override
64  */
65 cvox.SearchWidget.prototype.show = function() {
66   goog.base(this, 'show');
67   this.active = true;
68   this.hasMatch_ = false;
69   cvox.ChromeVox.navigationManager.setGranularity(
70       cvox.NavigationShifter.GRANULARITIES.OBJECT, true, false);
71
72   // Always start search forward.
73   cvox.ChromeVox.navigationManager.setReversed(false);
74
75   // During profiling, NavigationHistory was found to have a serious performance
76   // impact on search.
77   this.focusRecovery_ = cvox.ChromeVox.navigationManager.getFocusRecovery();
78   cvox.ChromeVox.navigationManager.setFocusRecovery(false);
79
80   var containerNode = this.createContainerNode_();
81   this.containerNode_ = containerNode;
82
83   var overlayNode = this.createOverlayNode_();
84   containerNode.appendChild(overlayNode);
85
86   var promptNode = document.createElement('span');
87   promptNode.innerHTML = this.PROMPT_;
88   overlayNode.appendChild(promptNode);
89
90   this.txtNode_ = this.createTextAreaNode_();
91
92   overlayNode.appendChild(this.txtNode_);
93
94   document.body.appendChild(containerNode);
95
96   this.txtNode_.focus();
97
98   window.setTimeout(function() {
99     containerNode.style['opacity'] = '1.0';
100   }, 0);
101 };
102
103
104 /**
105  * @override
106  */
107 cvox.SearchWidget.prototype.hide = function(opt_noSync) {
108   if (this.isActive()) {
109     var containerNode = this.containerNode_;
110     containerNode.style.opacity = '0.0';
111     window.setTimeout(function() {
112       document.body.removeChild(containerNode);
113     }, 1000);
114     this.txtNode_ = null;
115     cvox.SearchWidget.containerNode = null;
116     cvox.ChromeVox.navigationManager.setFocusRecovery(this.focusRecovery_);
117     this.active = false;
118   }
119
120   cvox.$m('choice_widget_exited').
121       andPause().
122       andMessage(this.getNameMsg()).
123       speakFlush();
124
125   if (!this.hasMatch_ || !opt_noSync) {
126     cvox.ChromeVox.navigationManager.updateSelToArbitraryNode(
127         this.initialNode);
128   }
129   cvox.ChromeVoxEventSuspender.withSuspendedEvents(goog.bind(
130       cvox.ChromeVox.navigationManager.syncAll,
131       cvox.ChromeVox.navigationManager))(true);
132   cvox.ChromeVox.navigationManager.speakDescriptionArray(
133       cvox.ChromeVox.navigationManager.getDescription(),
134       cvox.QueueMode.QUEUE,
135       null,
136       cvox.AbstractTts.PERSONALITY_ANNOUNCEMENT);
137
138   // Update on Braille too.
139   // TODO: Use line granularity in search so we can simply call
140   // cvox.ChromeVox.navigationManager.getBraille().write() instead.
141   var text = this.textFromCurrentDescription_();
142   cvox.ChromeVox.braille.write(new cvox.NavBraille({
143     text: text,
144     startIndex: 0,
145     endIndex: 0
146   }));
147
148   goog.base(this, 'hide', true);
149 };
150
151
152 /**
153  * @override
154  */
155 cvox.SearchWidget.prototype.getNameMsg = function() {
156   return ['search_widget_intro'];
157 };
158
159
160 /**
161  * @override
162  */
163 cvox.SearchWidget.prototype.getHelpMsg = function() {
164   return 'search_widget_intro_help';
165 };
166
167
168 /**
169  * @override
170  */
171 cvox.SearchWidget.prototype.onKeyDown = function(evt) {
172   if (!this.isActive()) {
173     return false;
174   }
175   var searchStr = this.txtNode_.value;
176   if (evt.keyCode == 8) { // Backspace
177     if (searchStr.length > 0) {
178       searchStr = searchStr.substring(0, searchStr.length - 1);
179       this.txtNode_.value = searchStr;
180       this.beginSearch_(searchStr);
181     } else {
182       cvox.ChromeVox.navigationManager.updateSelToArbitraryNode(
183           this.initialNode);
184       cvox.ChromeVox.navigationManager.syncAll();
185     }
186   } else if (evt.keyCode == 40) { // Down arrow
187     this.next_(searchStr, false);
188   } else if (evt.keyCode == 38) { // Up arrow
189     this.next_(searchStr, true);
190   } else if (evt.keyCode == 13) { // Enter
191     this.hide(true);
192   } else if (evt.keyCode == 27) { // Escape
193     this.hide(false);
194   } else if (evt.ctrlKey && evt.keyCode == 67) { // ctrl + c
195     this.toggleCaseSensitivity_();
196   } else {
197     return goog.base(this, 'onKeyDown', evt);
198   }
199   evt.preventDefault();
200   evt.stopPropagation();
201   return true;
202 };
203
204
205 /**
206  * Adds the letter the user typed to the search string and updates the search.
207  * @override
208  */
209 cvox.SearchWidget.prototype.onKeyPress = function(evt) {
210   if (!this.isActive()) {
211     return false;
212   }
213
214   this.txtNode_.value += String.fromCharCode(evt.charCode);
215   var searchStr = this.txtNode_.value;
216   this.beginSearch_(searchStr);
217   evt.preventDefault();
218   evt.stopPropagation();
219   return true;
220 };
221
222
223 /**
224  * Called when navigation occurs.
225  * Override this method to react to navigation caused by user input.
226  */
227 cvox.SearchWidget.prototype.onNavigate = function() {
228 };
229
230
231 /**
232  * Gets the predicate to apply to every search.
233  * @return {?function(Array.<Node>)} A predicate; if null, no predicate applies.
234  */
235 cvox.SearchWidget.prototype.getPredicate = function() {
236   return null;
237 };
238
239
240 /**
241  * Goes to the next or previous result. For use in AndroidVox.
242  * @param {boolean=} opt_reverse Whether to find the next result in reverse.
243  * @return {Array.<cvox.NavDescription>} The next result.
244  */
245 cvox.SearchWidget.prototype.nextResult = function(opt_reverse) {
246   if (!this.isActive()) {
247     return null;
248   }
249   var searchStr = this.txtNode_.value;
250   return this.next_(searchStr, opt_reverse);
251 };
252
253
254 /**
255  * Create the container node for the search overlay.
256  *
257  * @return {!Element} The new element, not yet added to the document.
258  * @private
259  */
260 cvox.SearchWidget.prototype.createContainerNode_ = function() {
261   var containerNode = document.createElement('div');
262   containerNode.id = 'cvox-search';
263   containerNode.style['position'] = 'fixed';
264   containerNode.style['top'] = '50%';
265   containerNode.style['left'] = '50%';
266   containerNode.style['-webkit-transition'] = 'all 0.3s ease-in';
267   containerNode.style['opacity'] = '0.0';
268   containerNode.style['z-index'] = '2147483647';
269   containerNode.setAttribute('aria-hidden', 'true');
270   return containerNode;
271 };
272
273
274 /**
275  * Create the search overlay. This should be a child of the node
276  * returned from createContainerNode.
277  *
278  * @return {!Element} The new element, not yet added to the document.
279  * @private
280  */
281 cvox.SearchWidget.prototype.createOverlayNode_ = function() {
282   var overlayNode = document.createElement('div');
283   overlayNode.style['position'] = 'relative';
284   overlayNode.style['left'] = '-50%';
285   overlayNode.style['top'] = '-40px';
286   overlayNode.style['line-height'] = '1.2em';
287   overlayNode.style['font-size'] = '20px';
288   overlayNode.style['padding'] = '30px';
289   overlayNode.style['min-width'] = '150px';
290   overlayNode.style['color'] = '#fff';
291   overlayNode.style['background-color'] = 'rgba(0, 0, 0, 0.7)';
292   overlayNode.style['border-radius'] = '10px';
293   return overlayNode;
294 };
295
296
297 /**
298  * Create the text area node. This should be the child of the node
299  * returned from createOverlayNode.
300  *
301  * @return {!Element} The new element, not yet added to the document.
302  * @private
303  */
304 cvox.SearchWidget.prototype.createTextAreaNode_ = function() {
305   var textNode = document.createElement('textarea');
306   textNode.setAttribute('aria-hidden', 'true');
307   textNode.setAttribute('rows', '1');
308   textNode.style['color'] = '#fff';
309   textNode.style['background-color'] = 'rgba(0, 0, 0, 0.7)';
310   textNode.style['vertical-align'] = 'middle';
311   textNode.addEventListener('textInput',
312     this.handleSearchChanged_, false);
313   return textNode;
314 };
315
316
317 /**
318  * Toggles whether or not searches are case sensitive.
319  * @private
320  */
321 cvox.SearchWidget.prototype.toggleCaseSensitivity_ = function() {
322   if (this.caseSensitive_) {
323     cvox.SearchWidget.caseSensitive_ = false;
324     cvox.ChromeVox.tts.speak(
325         cvox.ChromeVox.msgs.getMsg('ignoring_case'),
326         cvox.QueueMode.FLUSH, null);
327   } else {
328     this.caseSensitive_ = true;
329     cvox.ChromeVox.tts.speak(
330         cvox.ChromeVox.msgs.getMsg('case_sensitive'),
331         cvox.QueueMode.FLUSH, null);
332   }
333 };
334
335
336 /**
337  * Gets the next result.
338  *
339  * @param {string} searchStr The text to search for.
340  * @return {Array.<cvox.NavDescription>} The next result, in the form of
341  * NavDescriptions.
342  * @private
343  */
344 cvox.SearchWidget.prototype.getNextResult_ = function(searchStr) {
345   var r = cvox.ChromeVox.navigationManager.isReversed();
346   if (!this.caseSensitive_) {
347     searchStr = searchStr.toLowerCase();
348   }
349
350   cvox.ChromeVox.navigationManager.setGranularity(
351       cvox.NavigationShifter.GRANULARITIES.OBJECT, true, false);
352
353   do {
354     if (this.getPredicate()) {
355       var retNode = this.getPredicate()(cvox.DomUtil.getAncestors(
356           cvox.ChromeVox.navigationManager.getCurrentNode()));
357       if (!retNode) {
358         continue;
359       }
360     }
361
362     var descriptions = cvox.ChromeVox.navigationManager.getDescription();
363     for (var i = 0; i < descriptions.length; i++) {
364       var targetStr = this.caseSensitive_ ? descriptions[i].text :
365           descriptions[i].text.toLowerCase();
366       var targetIndex = targetStr.indexOf(searchStr);
367
368       // Surround search hit with pauses.
369       if (targetIndex != -1 && targetStr.length > searchStr.length) {
370         descriptions[i].text =
371             cvox.DomUtil.collapseWhitespace(
372                 targetStr.substring(0, targetIndex)) +
373             ', ' + searchStr + ', ' +
374             targetStr.substring(targetIndex + searchStr.length);
375         descriptions[i].text =
376             cvox.DomUtil.collapseWhitespace(descriptions[i].text);
377       }
378       if (targetIndex != -1) {
379         return descriptions;
380       }
381     }
382     cvox.ChromeVox.navigationManager.setReversed(r);
383   } while (cvox.ChromeVox.navigationManager.navigate(true,
384       cvox.NavigationShifter.GRANULARITIES.OBJECT));
385 };
386
387
388 /**
389  * Performs the search starting from the initial position.
390  *
391  * @param {string} searchStr The text to search for.
392  * @private
393  */
394 cvox.SearchWidget.prototype.beginSearch_ = function(searchStr) {
395   var result = this.getNextResult_(searchStr);
396   this.outputSearchResult_(result, searchStr);
397   this.onNavigate();
398 };
399
400
401 /**
402  * Goes to the next (directed) matching result.
403  *
404  * @param {string} searchStr The text to search for.
405  * @param {boolean=} opt_reversed The direction.
406  * @return {Array.<cvox.NavDescription>} The next result.
407  * @private
408  */
409 cvox.SearchWidget.prototype.next_ = function(searchStr, opt_reversed) {
410   cvox.ChromeVox.navigationManager.setReversed(!!opt_reversed);
411
412   var success = false;
413   if (this.getPredicate()) {
414     success = cvox.ChromeVox.navigationManager.findNext(
415         /** @type {function(Array.<Node>)} */ (this.getPredicate()));
416     // TODO(dtseng): findNext always seems to point direction forward!
417     cvox.ChromeVox.navigationManager.setReversed(!!opt_reversed);
418     if (!success) {
419       cvox.ChromeVox.navigationManager.syncToBeginning();
420       cvox.ChromeVox.earcons.playEarcon(cvox.AbstractEarcons.WRAP);
421       success = true;
422     }
423   } else {
424     success = cvox.ChromeVox.navigationManager.navigate(true);
425   }
426   var result = success ? this.getNextResult_(searchStr) : null;
427   this.outputSearchResult_(result, searchStr);
428   this.onNavigate();
429   return result;
430 };
431
432
433 /**
434  * Given a range corresponding to a search result, highlight the result,
435  * speak it, focus the node if applicable, and speak some instructions
436  * at the end.
437  *
438  * @param {Array.<cvox.NavDescription>} result The description of the next
439  * result. If null, no more results were found and an error will be presented.
440  * @param {string} searchStr The text to search for.
441  * @private
442  */
443 cvox.SearchWidget.prototype.outputSearchResult_ = function(result, searchStr) {
444   cvox.ChromeVox.tts.stop();
445   if (!result) {
446     cvox.ChromeVox.earcons.playEarcon(cvox.AbstractEarcons.WRAP);
447     this.hasMatch_ = false;
448     return;
449   }
450
451   this.hasMatch_ = true;
452
453   // Speak the modified description and some instructions.
454   cvox.ChromeVoxEventSuspender.withSuspendedEvents(goog.bind(
455       cvox.ChromeVox.navigationManager.syncAll,
456       cvox.ChromeVox.navigationManager))(true);
457
458   cvox.ChromeVox.navigationManager.speakDescriptionArray(
459       result,
460       cvox.QueueMode.FLUSH,
461       null,
462       cvox.AbstractTts.PERSONALITY_ANNOUNCEMENT);
463
464   cvox.ChromeVox.tts.speak(cvox.ChromeVox.msgs.getMsg('search_help_item'),
465                            cvox.QueueMode.QUEUE,
466                            cvox.AbstractTts.PERSONALITY_ANNOTATION);
467
468   // Output to Braille.
469   // TODO: Use line granularity in search so we can simply call
470   // cvox.ChromeVox.navigationManager.getBraille().write() instead.
471   this.outputSearchResultToBraille_(searchStr);
472 };
473
474
475 /**
476  * Writes the currently selected search result to Braille, with description
477  * text formatted for Braille display instead of speech.
478  *
479  * @param {string} searchStr The text to search for.
480  *    Should be in navigation manager's description.
481  * @private
482  */
483 cvox.SearchWidget.prototype.outputSearchResultToBraille_ = function(searchStr) {
484   // Construct object we can pass to Chromevox.braille to write.
485   // We concatenate the text together and set the "cursor"
486   // position to be at the end of search query string
487   // (consistent with editing text in a field).
488   var text = this.textFromCurrentDescription_();
489   var targetStr = this.caseSensitive_ ? text :
490           text.toLowerCase();
491   searchStr = this.caseSensitive_ ? searchStr : searchStr.toLowerCase();
492   var targetIndex = targetStr.indexOf(searchStr);
493   if (targetIndex == -1) {
494     console.log('Search string not in result when preparing for Braille.');
495     return;
496   }
497
498   // Mark the string as a search result by adding a prefix
499   // and adjust the targetIndex accordingly.
500   var oldLength = text.length;
501   text = cvox.ChromeVox.msgs.getMsg('mark_as_search_result_brl', [text]);
502   var newLength = text.length;
503   targetIndex += (newLength - oldLength);
504
505   // Write to Braille with cursor at the end of the search hit.
506   cvox.ChromeVox.braille.write(new cvox.NavBraille({
507     text: text,
508     startIndex: (targetIndex + searchStr.length),
509     endIndex: (targetIndex + searchStr.length)
510   }));
511 };
512
513
514 /**
515  * Returns the concatenated text from the current description in the
516  * NavigationManager.
517  * TODO: May not be needed after we just simply use line granularity in search,
518  * since this is mostly used to display the long search result descriptions on
519  * Braille.
520  * @return {string} The concatenated text from the current description.
521  * @private
522  */
523 cvox.SearchWidget.prototype.textFromCurrentDescription_ = function() {
524   var descriptions = cvox.ChromeVox.navigationManager.getDescription();
525   var text = '';
526   for (var i = 0; i < descriptions.length; i++) {
527     text += descriptions[i].text + ' ';
528   }
529   return text;
530 };
531
532 /**
533  * @param {Object} evt The onInput event that the function is handling.
534  * @private
535  */
536 cvox.SearchWidget.prototype.handleSearchChanged_ = function(evt) {
537   var searchStr = evt.target.value + evt.data;
538   cvox.SearchWidget.prototype.beginSearch_(searchStr);
539 };