Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / print_preview / search / destination_search.js
1 // Copyright (c) 2012 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 cr.define('print_preview', function() {
6   'use strict';
7
8   /**
9    * Component used for searching for a print destination.
10    * This is a modal dialog that allows the user to search and select a
11    * destination to print to. When a destination is selected, it is written to
12    * the destination store.
13    * @param {!print_preview.DestinationStore} destinationStore Data store
14    *     containing the destinations to search through.
15    * @param {!print_preview.UserInfo} userInfo Event target that contains
16    *     information about the logged in user.
17    * @param {!print_preview.Metrics} metrics Used to record usage statistics.
18    * @constructor
19    * @extends {print_preview.Component}
20    */
21   function DestinationSearch(destinationStore, userInfo, metrics) {
22     print_preview.Component.call(this);
23
24     /**
25      * Data store containing the destinations to search through.
26      * @type {!print_preview.DestinationStore}
27      * @private
28      */
29     this.destinationStore_ = destinationStore;
30
31     /**
32      * Event target that contains information about the logged in user.
33      * @type {!print_preview.UserInfo}
34      * @private
35      */
36     this.userInfo_ = userInfo;
37
38     /**
39      * Used to record usage statistics.
40      * @type {!print_preview.Metrics}
41      * @private
42      */
43     this.metrics_ = metrics;
44
45     /**
46      * Whether or not a UMA histogram for the register promo being shown was
47      * already recorded.
48      * @type {bool}
49      * @private
50      */
51     this.registerPromoShownMetricRecorded_ = false;
52
53     /**
54      * Search box used to search through the destination lists.
55      * @type {!print_preview.SearchBox}
56      * @private
57      */
58     this.searchBox_ = new print_preview.SearchBox();
59     this.addChild(this.searchBox_);
60
61     /**
62      * Destination list containing recent destinations.
63      * @type {!print_preview.DestinationList}
64      * @private
65      */
66     this.recentList_ = new print_preview.RecentDestinationList(this);
67     this.addChild(this.recentList_);
68
69     /**
70      * Destination list containing local destinations.
71      * @type {!print_preview.DestinationList}
72      * @private
73      */
74     this.localList_ = new print_preview.DestinationList(
75         this,
76         localStrings.getString('localDestinationsTitle'),
77         cr.isChromeOS ? null : localStrings.getString('manage'));
78     this.addChild(this.localList_);
79
80     /**
81      * Destination list containing cloud destinations.
82      * @type {!print_preview.DestinationList}
83      * @private
84      */
85     this.cloudList_ = new print_preview.CloudDestinationList(this);
86     this.addChild(this.cloudList_);
87   };
88
89   /**
90    * Event types dispatched by the component.
91    * @enum {string}
92    */
93   DestinationSearch.EventType = {
94     // Dispatched when user requests to sign-in into another Google account.
95     ADD_ACCOUNT: 'print_preview.DestinationSearch.ADD_ACCOUNT',
96
97     // Dispatched when the user requests to manage their cloud destinations.
98     MANAGE_CLOUD_DESTINATIONS:
99         'print_preview.DestinationSearch.MANAGE_CLOUD_DESTINATIONS',
100
101     // Dispatched when the user requests to manage their local destinations.
102     MANAGE_LOCAL_DESTINATIONS:
103         'print_preview.DestinationSearch.MANAGE_LOCAL_DESTINATIONS',
104
105     // Dispatched when the user requests to sign-in to their Google account.
106     SIGN_IN: 'print_preview.DestinationSearch.SIGN_IN'
107   };
108
109   /**
110    * Padding at the bottom of a destination list in pixels.
111    * @type {number}
112    * @const
113    * @private
114    */
115   DestinationSearch.LIST_BOTTOM_PADDING_ = 18;
116
117   /**
118    * Number of unregistered destinations that may be promoted to the top.
119    * @type {number}
120    * @const
121    * @private
122    */
123   DestinationSearch.MAX_PROMOTED_UNREGISTERED_PRINTERS_ = 2;
124
125   DestinationSearch.prototype = {
126     __proto__: print_preview.Component.prototype,
127
128     /** @return {boolean} Whether the component is visible. */
129     getIsVisible: function() {
130       return !this.getElement().classList.contains('transparent');
131     },
132
133     /** @param {boolean} isVisible Whether the component is visible. */
134     setIsVisible: function(isVisible) {
135       if (this.getIsVisible() == isVisible) {
136         return;
137       }
138       if (isVisible) {
139         setIsVisible(this.getElement(), true);
140         setTimeout(function(element) {
141           element.classList.remove('transparent');
142         }.bind(this, this.getElement()), 0);
143         this.searchBox_.focus();
144         var promoEl = this.getChildElement('.cloudprint-promo');
145         if (getIsVisible(promoEl)) {
146           this.metrics_.incrementDestinationSearchBucket(
147               print_preview.Metrics.DestinationSearchBucket.
148                   CLOUDPRINT_PROMO_SHOWN);
149         }
150         if (this.userInfo_.initialized) {
151           this.onUsersChanged_();
152         }
153         this.reflowLists_();
154       } else {
155         this.getElement().classList.add('transparent');
156         // Collapse all destination lists
157         this.localList_.setIsShowAll(false);
158         this.cloudList_.setIsShowAll(false);
159         this.searchBox_.setQuery('');
160         this.filterLists_(null);
161       }
162     },
163
164     /** Shows the Google Cloud Print promotion banner. */
165     showCloudPrintPromo: function() {
166       setIsVisible(this.getChildElement('.cloudprint-promo'), true);
167       if (this.getIsVisible()) {
168         this.metrics_.incrementDestinationSearchBucket(
169             print_preview.Metrics.DestinationSearchBucket.
170                 CLOUDPRINT_PROMO_SHOWN);
171       }
172       this.reflowLists_();
173     },
174
175     /** @override */
176     enterDocument: function() {
177       print_preview.Component.prototype.enterDocument.call(this);
178
179       this.getElement().addEventListener('webkitTransitionEnd', function f(e) {
180         if (e.target != e.currentTarget || e.propertyName != 'opacity')
181           return;
182         if (e.target.classList.contains('transparent')) {
183           setIsVisible(e.target, false);
184         }
185       });
186
187       this.tracker.add(
188           this.getChildElement('.account-select'),
189           'change',
190           this.onAccountChange_.bind(this));
191
192       this.tracker.add(
193           this.getChildElement('.page > .close-button'),
194           'click',
195           this.onCloseClick_.bind(this));
196
197       this.tracker.add(
198           this.getChildElement('.sign-in'),
199           'click',
200           this.onSignInActivated_.bind(this));
201
202       this.tracker.add(
203           this.getChildElement('.cloudprint-promo > .close-button'),
204           'click',
205           this.onCloudprintPromoCloseButtonClick_.bind(this));
206       this.tracker.add(
207           this.searchBox_,
208           print_preview.SearchBox.EventType.SEARCH,
209           this.onSearch_.bind(this));
210       this.tracker.add(
211           this,
212           print_preview.DestinationListItem.EventType.SELECT,
213           this.onDestinationSelect_.bind(this));
214
215       this.tracker.add(
216           this.destinationStore_,
217           print_preview.DestinationStore.EventType.DESTINATIONS_INSERTED,
218           this.onDestinationsInserted_.bind(this));
219       this.tracker.add(
220           this.destinationStore_,
221           print_preview.DestinationStore.EventType.DESTINATION_SELECT,
222           this.onDestinationStoreSelect_.bind(this));
223       this.tracker.add(
224           this.destinationStore_,
225           print_preview.DestinationStore.EventType.DESTINATION_SEARCH_STARTED,
226           this.updateThrobbers_.bind(this));
227       this.tracker.add(
228           this.destinationStore_,
229           print_preview.DestinationStore.EventType.DESTINATION_SEARCH_DONE,
230           this.onDestinationSearchDone_.bind(this));
231
232       this.tracker.add(
233           this.localList_,
234           print_preview.DestinationList.EventType.ACTION_LINK_ACTIVATED,
235           this.onManageLocalDestinationsActivated_.bind(this));
236       this.tracker.add(
237           this.cloudList_,
238           print_preview.DestinationList.EventType.ACTION_LINK_ACTIVATED,
239           this.onManageCloudDestinationsActivated_.bind(this));
240
241       this.tracker.add(
242           this.getElement(), 'click', this.onClick_.bind(this));
243       this.tracker.add(
244           this.getChildElement('.page'),
245           'webkitAnimationEnd',
246           this.onAnimationEnd_.bind(this));
247
248       this.tracker.add(
249           this.userInfo_,
250           print_preview.UserInfo.EventType.USERS_CHANGED,
251           this.onUsersChanged_.bind(this));
252
253       this.tracker.add(window, 'resize', this.onWindowResize_.bind(this));
254
255       this.updateThrobbers_();
256
257       // Render any destinations already in the store.
258       this.renderDestinations_();
259     },
260
261     /** @override */
262     decorateInternal: function() {
263       this.searchBox_.decorate($('search-box'));
264       this.recentList_.render(this.getChildElement('.recent-list'));
265       this.localList_.render(this.getChildElement('.local-list'));
266       this.cloudList_.render(this.getChildElement('.cloud-list'));
267       this.getChildElement('.promo-text').innerHTML = localStrings.getStringF(
268           'cloudPrintPromotion',
269           '<span class="sign-in link-button">',
270           '</span>');
271       this.getChildElement('.account-select-label').textContent =
272           localStrings.getString('accountSelectTitle');
273     },
274
275     /**
276      * @return {number} Height available for destination lists, in pixels.
277      * @private
278      */
279     getAvailableListsHeight_: function() {
280       var elStyle = window.getComputedStyle(this.getElement());
281       return this.getElement().offsetHeight -
282           parseInt(elStyle.getPropertyValue('padding-top')) -
283           parseInt(elStyle.getPropertyValue('padding-bottom')) -
284           this.getChildElement('.lists').offsetTop -
285           this.getChildElement('.cloudprint-promo').offsetHeight;
286     },
287
288     /**
289      * Filters all destination lists with the given query.
290      * @param {?string} query Query to filter destination lists by.
291      * @private
292      */
293     filterLists_: function(query) {
294       this.recentList_.updateSearchQuery(query);
295       this.localList_.updateSearchQuery(query);
296       this.cloudList_.updateSearchQuery(query);
297     },
298
299     /**
300      * Resets the search query.
301      * @private
302      */
303     resetSearch_: function() {
304       this.searchBox_.setQuery(null);
305       this.filterLists_(null);
306     },
307
308     /**
309      * Renders all of the destinations in the destination store.
310      * @private
311      */
312     renderDestinations_: function() {
313       var recentDestinations = [];
314       var localDestinations = [];
315       var cloudDestinations = [];
316       var unregisteredCloudDestinations = [];
317
318       var destinations =
319           this.destinationStore_.destinations(this.userInfo_.activeUser);
320       destinations.forEach(function(destination) {
321         if (destination.isRecent) {
322           recentDestinations.push(destination);
323         }
324         if (destination.isLocal ||
325             destination.origin == print_preview.Destination.Origin.DEVICE) {
326           localDestinations.push(destination);
327         } else {
328           if (destination.connectionStatus ==
329                 print_preview.Destination.ConnectionStatus.UNREGISTERED) {
330             unregisteredCloudDestinations.push(destination);
331           } else {
332             cloudDestinations.push(destination);
333           }
334         }
335       });
336
337       if (unregisteredCloudDestinations.length != 0 &&
338           !this.registerPromoShownMetricRecorded_) {
339         this.metrics_.incrementDestinationSearchBucket(
340           print_preview.Metrics.DestinationSearchBucket.REGISTER_PROMO_SHOWN);
341         this.registerPromoShownMetricRecorded_ = true;
342       }
343
344       var finalCloudDestinations = unregisteredCloudDestinations.slice(
345         0, DestinationSearch.MAX_PROMOTED_UNREGISTERED_PRINTERS_).concat(
346           cloudDestinations,
347           unregisteredCloudDestinations.slice(
348             DestinationSearch.MAX_PROMOTED_UNREGISTERED_PRINTERS_));
349
350       this.recentList_.updateDestinations(recentDestinations);
351       this.localList_.updateDestinations(localDestinations);
352       this.cloudList_.updateDestinations(finalCloudDestinations);
353     },
354
355     /**
356      * Reflows the destination lists according to the available height.
357      * @private
358      */
359     reflowLists_: function() {
360       if (!this.getIsVisible()) {
361         return;
362       }
363
364       var hasCloudList = getIsVisible(this.getChildElement('.cloud-list'));
365       var lists = [this.recentList_, this.localList_];
366       if (hasCloudList) {
367         lists.push(this.cloudList_);
368       }
369
370       var getListsTotalHeight = function(lists, counts) {
371         return lists.reduce(function(sum, list, index) {
372           return sum + list.getEstimatedHeightInPixels(counts[index]) +
373               DestinationSearch.LIST_BOTTOM_PADDING_;
374         }, 0);
375       };
376       var getCounts = function(lists, count) {
377         return lists.map(function(list) { return count; });
378       };
379
380       var availableHeight = this.getAvailableListsHeight_();
381       var listsEl = this.getChildElement('.lists');
382       listsEl.style.maxHeight = availableHeight + 'px';
383
384       var maxListLength = lists.reduce(function(prevCount, list) {
385         return Math.max(prevCount, list.getDestinationsCount());
386       }, 0);
387       for (var i = 1; i <= maxListLength; i++) {
388         if (getListsTotalHeight(lists, getCounts(lists, i)) > availableHeight) {
389           i--;
390           break;
391         }
392       }
393       var counts = getCounts(lists, i);
394       // Fill up the possible n-1 free slots left by the previous loop.
395       if (getListsTotalHeight(lists, counts) < availableHeight) {
396         for (var countIndex = 0; countIndex < counts.length; countIndex++) {
397           counts[countIndex]++;
398           if (getListsTotalHeight(lists, counts) > availableHeight) {
399             counts[countIndex]--;
400             break;
401           }
402         }
403       }
404
405       lists.forEach(function(list, index) {
406         list.updateShortListSize(counts[index]);
407       });
408
409       // Set height of the list manually so that search filter doesn't change
410       // lists height.
411       var listsHeight = getListsTotalHeight(lists, counts) + 'px';
412       if (listsHeight != listsEl.style.height) {
413         // Try to close account select if there's a possibility it's open now.
414         var accountSelectEl = this.getChildElement('.account-select');
415         if (!accountSelectEl.disabled) {
416           accountSelectEl.disabled = true;
417           accountSelectEl.disabled = false;
418         }
419         listsEl.style.height = listsHeight;
420       }
421     },
422
423     /**
424      * Updates whether the throbbers for the various destination lists should be
425      * shown or hidden.
426      * @private
427      */
428     updateThrobbers_: function() {
429       this.localList_.setIsThrobberVisible(
430           this.destinationStore_.isLocalDestinationSearchInProgress);
431       this.cloudList_.setIsThrobberVisible(
432           this.destinationStore_.isCloudDestinationSearchInProgress);
433       this.recentList_.setIsThrobberVisible(
434           this.destinationStore_.isLocalDestinationSearchInProgress &&
435           this.destinationStore_.isCloudDestinationSearchInProgress);
436       this.reflowLists_();
437     },
438
439     /**
440      * Called when user's logged in accounts change. Updates the UI.
441      * @private
442      */
443     onUsersChanged_: function() {
444       var loggedIn = this.userInfo_.loggedIn;
445       if (loggedIn) {
446         var accountSelectEl = this.getChildElement('.account-select');
447         accountSelectEl.innerHTML = '';
448         this.userInfo_.users.forEach(function(account) {
449           var option = document.createElement('option');
450           option.text = account;
451           option.value = account;
452           accountSelectEl.add(option);
453         });
454         var option = document.createElement('option');
455         option.text = localStrings.getString('addAccountTitle');
456         option.value = '';
457         accountSelectEl.add(option);
458
459         accountSelectEl.selectedIndex =
460             this.userInfo_.users.indexOf(this.userInfo_.activeUser);
461       }
462
463       setIsVisible(this.getChildElement('.user-info'), loggedIn);
464       setIsVisible(this.getChildElement('.cloud-list'), loggedIn);
465       setIsVisible(this.getChildElement('.cloudprint-promo'), !loggedIn);
466       this.reflowLists_();
467     },
468
469     /**
470      * Called when a destination search should be executed. Filters the
471      * destination lists with the given query.
472      * @param {Event} evt Contains the search query.
473      * @private
474      */
475     onSearch_: function(evt) {
476       this.filterLists_(evt.query);
477     },
478
479     /**
480      * Called when the close button is clicked. Hides the search widget.
481      * @private
482      */
483     onCloseClick_: function() {
484       this.setIsVisible(false);
485       this.resetSearch_();
486       this.metrics_.incrementDestinationSearchBucket(
487           print_preview.Metrics.DestinationSearchBucket.CANCELED);
488     },
489
490     /**
491      * Called when a destination is selected. Clears the search and hides the
492      * widget.
493      * @param {Event} evt Contains the selected destination.
494      * @private
495      */
496     onDestinationSelect_: function(evt) {
497       this.setIsVisible(false);
498       this.resetSearch_();
499       this.destinationStore_.selectDestination(evt.destination);
500       this.metrics_.incrementDestinationSearchBucket(
501           print_preview.Metrics.DestinationSearchBucket.DESTINATION_SELECTED);
502     },
503
504     /**
505      * Called when a destination is selected. Selected destination are marked as
506      * recent, so we have to update our recent destinations list.
507      * @private
508      */
509     onDestinationStoreSelect_: function() {
510       var destinations =
511           this.destinationStore_.destinations(this.userInfo_.activeUser);
512       var recentDestinations = [];
513       destinations.forEach(function(destination) {
514         if (destination.isRecent) {
515           recentDestinations.push(destination);
516         }
517       });
518       this.recentList_.updateDestinations(recentDestinations);
519       this.reflowLists_();
520     },
521
522     /**
523      * Called when destinations are inserted into the store. Rerenders
524      * destinations.
525      * @private
526      */
527     onDestinationsInserted_: function() {
528       this.renderDestinations_();
529       this.reflowLists_();
530     },
531
532     /**
533      * Called when destinations are inserted into the store. Rerenders
534      * destinations.
535      * @private
536      */
537     onDestinationSearchDone_: function() {
538       this.updateThrobbers_();
539       this.renderDestinations_();
540       this.reflowLists_();
541     },
542
543     /**
544      * Called when the manage cloud printers action is activated.
545      * @private
546      */
547     onManageCloudDestinationsActivated_: function() {
548       cr.dispatchSimpleEvent(
549           this,
550           print_preview.DestinationSearch.EventType.MANAGE_CLOUD_DESTINATIONS);
551     },
552
553     /**
554      * Called when the manage local printers action is activated.
555      * @private
556      */
557     onManageLocalDestinationsActivated_: function() {
558       cr.dispatchSimpleEvent(
559           this,
560           print_preview.DestinationSearch.EventType.MANAGE_LOCAL_DESTINATIONS);
561     },
562
563     /**
564      * Called when the "Sign in" link on the Google Cloud Print promo is
565      * activated.
566      * @private
567      */
568     onSignInActivated_: function() {
569       cr.dispatchSimpleEvent(this, DestinationSearch.EventType.SIGN_IN);
570       this.metrics_.incrementDestinationSearchBucket(
571           print_preview.Metrics.DestinationSearchBucket.SIGNIN_TRIGGERED);
572     },
573
574     /**
575      * Called when item in the Accounts list is selected. Initiates active user
576      * switch or, for 'Add account...' item, opens Google sign-in page.
577      * @private
578      */
579     onAccountChange_: function() {
580       var accountSelectEl = this.getChildElement('.account-select');
581       var account =
582           accountSelectEl.options[accountSelectEl.selectedIndex].value;
583       if (account) {
584         this.userInfo_.activeUser = account;
585         this.destinationStore_.reloadUserCookieBasedDestinations();
586         this.metrics_.incrementDestinationSearchBucket(
587             print_preview.Metrics.DestinationSearchBucket.ACCOUNT_CHANGED);
588       } else {
589         cr.dispatchSimpleEvent(this, DestinationSearch.EventType.ADD_ACCOUNT);
590         // Set selection back to the active user.
591         for (var i = 0; i < accountSelectEl.options.length; i++) {
592           if (accountSelectEl.options[i].value == this.userInfo_.activeUser) {
593             accountSelectEl.selectedIndex = i;
594             break;
595           }
596         }
597         this.metrics_.incrementDestinationSearchBucket(
598             print_preview.Metrics.DestinationSearchBucket.ADD_ACCOUNT_SELECTED);
599       }
600     },
601
602     /**
603      * Called when the close button on the cloud print promo is clicked. Hides
604      * the promo.
605      * @private
606      */
607     onCloudprintPromoCloseButtonClick_: function() {
608       setIsVisible(this.getChildElement('.cloudprint-promo'), false);
609     },
610
611     /**
612      * Called when the overlay is clicked. Pulses the page.
613      * @param {Event} event Contains the element that was clicked.
614      * @private
615      */
616     onClick_: function(event) {
617       if (event.target == this.getElement()) {
618         this.getChildElement('.page').classList.add('pulse');
619       }
620     },
621
622     /**
623      * Called when an animation ends on the page.
624      * @private
625      */
626     onAnimationEnd_: function() {
627       this.getChildElement('.page').classList.remove('pulse');
628     },
629
630     /**
631      * Called when the window is resized. Reflows layout of destination lists.
632      * @private
633      */
634     onWindowResize_: function() {
635       this.reflowLists_();
636     }
637   };
638
639   // Export
640   return {
641     DestinationSearch: DestinationSearch
642   };
643 });