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