Upstream version 11.40.277.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.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           '<a is="action-link" class="sign-in">',
284           '</a>');
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'), 10) -
297           parseInt(elStyle.getPropertyValue('padding-bottom'), 10) -
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             destination.origin == print_preview.Destination.Origin.DEVICE) {
341           localDestinations.push(destination);
342         } else {
343           if (destination.connectionStatus ==
344                 print_preview.Destination.ConnectionStatus.UNREGISTERED) {
345             unregisteredCloudDestinations.push(destination);
346           } else {
347             cloudDestinations.push(destination);
348           }
349         }
350       });
351
352       if (unregisteredCloudDestinations.length != 0 &&
353           !this.registerPromoShownMetricRecorded_) {
354         this.metrics_.record(
355             print_preview.Metrics.DestinationSearchBucket.REGISTER_PROMO_SHOWN);
356         this.registerPromoShownMetricRecorded_ = true;
357       }
358
359       var finalCloudDestinations = unregisteredCloudDestinations.slice(
360         0, DestinationSearch.MAX_PROMOTED_UNREGISTERED_PRINTERS_).concat(
361           cloudDestinations,
362           unregisteredCloudDestinations.slice(
363             DestinationSearch.MAX_PROMOTED_UNREGISTERED_PRINTERS_));
364
365       this.recentList_.updateDestinations(recentDestinations);
366       this.localList_.updateDestinations(localDestinations);
367       this.cloudList_.updateDestinations(finalCloudDestinations);
368     },
369
370     /**
371      * Reflows the destination lists according to the available height.
372      * @private
373      */
374     reflowLists_: function() {
375       if (!this.getIsVisible()) {
376         return;
377       }
378
379       var hasCloudList = getIsVisible(this.getChildElement('.cloud-list'));
380       var lists = [this.recentList_, this.localList_];
381       if (hasCloudList) {
382         lists.push(this.cloudList_);
383       }
384
385       var getListsTotalHeight = function(lists, counts) {
386         return lists.reduce(function(sum, list, index) {
387           return sum + list.getEstimatedHeightInPixels(counts[index]) +
388               DestinationSearch.LIST_BOTTOM_PADDING_;
389         }, 0);
390       };
391       var getCounts = function(lists, count) {
392         return lists.map(function(list) { return count; });
393       };
394
395       var availableHeight = this.getAvailableListsHeight_();
396       var listsEl = this.getChildElement('.lists');
397       listsEl.style.maxHeight = availableHeight + 'px';
398
399       var maxListLength = lists.reduce(function(prevCount, list) {
400         return Math.max(prevCount, list.getDestinationsCount());
401       }, 0);
402       for (var i = 1; i <= maxListLength; i++) {
403         if (getListsTotalHeight(lists, getCounts(lists, i)) > availableHeight) {
404           i--;
405           break;
406         }
407       }
408       var counts = getCounts(lists, i);
409       // Fill up the possible n-1 free slots left by the previous loop.
410       if (getListsTotalHeight(lists, counts) < availableHeight) {
411         for (var countIndex = 0; countIndex < counts.length; countIndex++) {
412           counts[countIndex]++;
413           if (getListsTotalHeight(lists, counts) > availableHeight) {
414             counts[countIndex]--;
415             break;
416           }
417         }
418       }
419
420       lists.forEach(function(list, index) {
421         list.updateShortListSize(counts[index]);
422       });
423
424       // Set height of the list manually so that search filter doesn't change
425       // lists height.
426       var listsHeight = getListsTotalHeight(lists, counts) + 'px';
427       if (listsHeight != listsEl.style.height) {
428         // Try to close account select if there's a possibility it's open now.
429         var accountSelectEl = this.getChildElement('.account-select');
430         if (!accountSelectEl.disabled) {
431           accountSelectEl.disabled = true;
432           accountSelectEl.disabled = false;
433         }
434         listsEl.style.height = listsHeight;
435       }
436     },
437
438     /**
439      * Updates whether the throbbers for the various destination lists should be
440      * shown or hidden.
441      * @private
442      */
443     updateThrobbers_: function() {
444       this.localList_.setIsThrobberVisible(
445           this.destinationStore_.isLocalDestinationSearchInProgress);
446       this.cloudList_.setIsThrobberVisible(
447           this.destinationStore_.isCloudDestinationSearchInProgress);
448       this.recentList_.setIsThrobberVisible(
449           this.destinationStore_.isLocalDestinationSearchInProgress &&
450           this.destinationStore_.isCloudDestinationSearchInProgress);
451       this.reflowLists_();
452     },
453
454     /**
455      * Updates printer sharing invitations UI.
456      * @private
457      */
458     updateInvitations_: function() {
459       var invitations = this.userInfo_.activeUser ?
460           this.invitationStore_.invitations(this.userInfo_.activeUser) : [];
461       if (invitations.length > 0) {
462         if (this.invitation_ != invitations[0]) {
463           this.metrics_.record(print_preview.Metrics.DestinationSearchBucket.
464               INVITATION_AVAILABLE);
465         }
466         this.invitation_ = invitations[0];
467         this.showInvitation_(this.invitation_);
468       } else {
469         this.invitation_ = null;
470       }
471       setIsVisible(
472           this.getChildElement('.invitation-container'), !!this.invitation_);
473       this.reflowLists_();
474     },
475
476     /**
477      * @param {!printe_preview.Invitation} invitation Invitation to show.
478      * @private
479      */
480     showInvitation_: function(invitation) {
481       var invitationText = '';
482       if (invitation.asGroupManager) {
483         invitationText = loadTimeData.getStringF(
484             'groupPrinterSharingInviteText',
485             invitation.sender,
486             invitation.destination.displayName,
487             invitation.receiver);
488       } else {
489         invitationText = loadTimeData.getStringF(
490             'printerSharingInviteText',
491             invitation.sender,
492             invitation.destination.displayName);
493       }
494       this.getChildElement('.invitation-text').innerHTML = invitationText;
495
496       var acceptButton = this.getChildElement('.invitation-accept-button');
497       acceptButton.textContent = loadTimeData.getString(
498           invitation.asGroupManager ? 'acceptForGroup' : 'accept');
499       acceptButton.disabled = !!this.invitationStore_.invitationInProgress;
500       this.getChildElement('.invitation-reject-button').disabled =
501           !!this.invitationStore_.invitationInProgress;
502       setIsVisible(
503           this.getChildElement('#invitation-process-throbber'),
504           !!this.invitationStore_.invitationInProgress);
505     },
506
507     /**
508      * Called when user's logged in accounts change. Updates the UI.
509      * @private
510      */
511     onUsersChanged_: function() {
512       var loggedIn = this.userInfo_.loggedIn;
513       if (loggedIn) {
514         var accountSelectEl = this.getChildElement('.account-select');
515         accountSelectEl.innerHTML = '';
516         this.userInfo_.users.forEach(function(account) {
517           var option = document.createElement('option');
518           option.text = account;
519           option.value = account;
520           accountSelectEl.add(option);
521         });
522         var option = document.createElement('option');
523         option.text = loadTimeData.getString('addAccountTitle');
524         option.value = '';
525         accountSelectEl.add(option);
526
527         accountSelectEl.selectedIndex =
528             this.userInfo_.users.indexOf(this.userInfo_.activeUser);
529       }
530
531       setIsVisible(this.getChildElement('.user-info'), loggedIn);
532       setIsVisible(this.getChildElement('.cloud-list'), loggedIn);
533       setIsVisible(this.getChildElement('.cloudprint-promo'), !loggedIn);
534       this.updateInvitations_();
535     },
536
537     /**
538      * Called when a destination search should be executed. Filters the
539      * destination lists with the given query.
540      * @param {Event} evt Contains the search query.
541      * @private
542      */
543     onSearch_: function(evt) {
544       this.filterLists_(evt.queryRegExp);
545     },
546
547     /**
548      * Called when a destination is selected. Clears the search and hides the
549      * widget.
550      * @param {Event} evt Contains the selected destination.
551      * @private
552      */
553     onDestinationSelect_: function(evt) {
554       this.setIsVisible(false);
555       this.destinationStore_.selectDestination(evt.destination);
556       this.metrics_.record(print_preview.Metrics.DestinationSearchBucket.
557           DESTINATION_CLOSED_CHANGED);
558     },
559
560     /**
561      * Called when a destination is selected. Selected destination are marked as
562      * recent, so we have to update our recent destinations list.
563      * @private
564      */
565     onDestinationStoreSelect_: function() {
566       var destinations =
567           this.destinationStore_.destinations(this.userInfo_.activeUser);
568       var recentDestinations = [];
569       destinations.forEach(function(destination) {
570         if (destination.isRecent) {
571           recentDestinations.push(destination);
572         }
573       });
574       this.recentList_.updateDestinations(recentDestinations);
575       this.reflowLists_();
576     },
577
578     /**
579      * Called when destinations are inserted into the store. Rerenders
580      * destinations.
581      * @private
582      */
583     onDestinationsInserted_: function() {
584       this.renderDestinations_();
585       this.reflowLists_();
586     },
587
588     /**
589      * Called when destinations are inserted into the store. Rerenders
590      * destinations.
591      * @private
592      */
593     onDestinationSearchDone_: function() {
594       this.updateThrobbers_();
595       this.renderDestinations_();
596       this.reflowLists_();
597       // In case user account information was retrieved with this search
598       // (knowing current user account is required to fetch invitations).
599       this.invitationStore_.startLoadingInvitations();
600     },
601
602     /**
603      * Called when the manage cloud printers action is activated.
604      * @private
605      */
606     onManageCloudDestinationsActivated_: function() {
607       cr.dispatchSimpleEvent(
608           this,
609           print_preview.DestinationSearch.EventType.MANAGE_CLOUD_DESTINATIONS);
610     },
611
612     /**
613      * Called when the manage local printers action is activated.
614      * @private
615      */
616     onManageLocalDestinationsActivated_: function() {
617       cr.dispatchSimpleEvent(
618           this,
619           print_preview.DestinationSearch.EventType.MANAGE_LOCAL_DESTINATIONS);
620     },
621
622     /**
623      * Called when the "Sign in" link on the Google Cloud Print promo is
624      * activated.
625      * @private
626      */
627     onSignInActivated_: function() {
628       cr.dispatchSimpleEvent(this, DestinationSearch.EventType.SIGN_IN);
629       this.metrics_.record(
630           print_preview.Metrics.DestinationSearchBucket.SIGNIN_TRIGGERED);
631     },
632
633     /**
634      * Called when item in the Accounts list is selected. Initiates active user
635      * switch or, for 'Add account...' item, opens Google sign-in page.
636      * @private
637      */
638     onAccountChange_: function() {
639       var accountSelectEl = this.getChildElement('.account-select');
640       var account =
641           accountSelectEl.options[accountSelectEl.selectedIndex].value;
642       if (account) {
643         this.userInfo_.activeUser = account;
644         this.destinationStore_.reloadUserCookieBasedDestinations();
645         this.invitationStore_.startLoadingInvitations();
646         this.metrics_.record(
647             print_preview.Metrics.DestinationSearchBucket.ACCOUNT_CHANGED);
648       } else {
649         cr.dispatchSimpleEvent(this, DestinationSearch.EventType.ADD_ACCOUNT);
650         // Set selection back to the active user.
651         for (var i = 0; i < accountSelectEl.options.length; i++) {
652           if (accountSelectEl.options[i].value == this.userInfo_.activeUser) {
653             accountSelectEl.selectedIndex = i;
654             break;
655           }
656         }
657         this.metrics_.record(
658             print_preview.Metrics.DestinationSearchBucket.ADD_ACCOUNT_SELECTED);
659       }
660     },
661
662     /**
663      * Called when the printer sharing invitation Accept/Reject button is
664      * clicked.
665      * @private
666      */
667     onInvitationProcessButtonClick_: function(accept) {
668       this.metrics_.record(accept ?
669           print_preview.Metrics.DestinationSearchBucket.INVITATION_ACCEPTED :
670           print_preview.Metrics.DestinationSearchBucket.INVITATION_REJECTED);
671       this.invitationStore_.processInvitation(this.invitation_, accept);
672       this.updateInvitations_();
673     },
674
675     /**
676      * Called when the close button on the cloud print promo is clicked. Hides
677      * the promo.
678      * @private
679      */
680     onCloudprintPromoCloseButtonClick_: function() {
681       setIsVisible(this.getChildElement('.cloudprint-promo'), false);
682     },
683
684     /**
685      * Called when the window is resized. Reflows layout of destination lists.
686      * @private
687      */
688     onWindowResize_: function() {
689       this.reflowLists_();
690     }
691   };
692
693   // Export
694   return {
695     DestinationSearch: DestinationSearch
696   };
697 });