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.
5 cr.define('print_preview', function() {
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.
20 * @extends {print_preview.Overlay}
22 function DestinationSearch(destinationStore, invitationStore, userInfo) {
23 print_preview.Overlay.call(this);
26 * Data store containing the destinations to search through.
27 * @type {!print_preview.DestinationStore}
30 this.destinationStore_ = destinationStore;
33 * Data store holding printer sharing invitations.
34 * @type {!print_preview.DestinationStore}
37 this.invitationStore_ = invitationStore;
40 * Event target that contains information about the logged in user.
41 * @type {!print_preview.UserInfo}
44 this.userInfo_ = userInfo;
47 * Currently displayed printer sharing invitation.
48 * @type {print_preview.Invitation}
51 this.invitation_ = null;
54 * Used to record usage statistics.
55 * @type {!print_preview.DestinationSearchMetricsContext}
58 this.metrics_ = new print_preview.DestinationSearchMetricsContext();
61 * Whether or not a UMA histogram for the register promo being shown was
66 this.registerPromoShownMetricRecorded_ = false;
69 * Search box used to search through the destination lists.
70 * @type {!print_preview.SearchBox}
73 this.searchBox_ = new print_preview.SearchBox(
74 loadTimeData.getString('searchBoxPlaceholder'));
75 this.addChild(this.searchBox_);
78 * Destination list containing recent destinations.
79 * @type {!print_preview.DestinationList}
82 this.recentList_ = new print_preview.RecentDestinationList(this);
83 this.addChild(this.recentList_);
86 * Destination list containing local destinations.
87 * @type {!print_preview.DestinationList}
90 this.localList_ = new print_preview.DestinationList(
92 loadTimeData.getString('localDestinationsTitle'),
93 cr.isChromeOS ? null : loadTimeData.getString('manage'));
94 this.addChild(this.localList_);
97 * Destination list containing cloud destinations.
98 * @type {!print_preview.DestinationList}
101 this.cloudList_ = new print_preview.CloudDestinationList(this);
102 this.addChild(this.cloudList_);
106 * Event types dispatched by the component.
109 DestinationSearch.EventType = {
110 // Dispatched when user requests to sign-in into another Google account.
111 ADD_ACCOUNT: 'print_preview.DestinationSearch.ADD_ACCOUNT',
113 // Dispatched when the user requests to manage their cloud destinations.
114 MANAGE_CLOUD_DESTINATIONS:
115 'print_preview.DestinationSearch.MANAGE_CLOUD_DESTINATIONS',
117 // Dispatched when the user requests to manage their local destinations.
118 MANAGE_LOCAL_DESTINATIONS:
119 'print_preview.DestinationSearch.MANAGE_LOCAL_DESTINATIONS',
121 // Dispatched when the user requests to sign-in to their Google account.
122 SIGN_IN: 'print_preview.DestinationSearch.SIGN_IN'
126 * Padding at the bottom of a destination list in pixels.
131 DestinationSearch.LIST_BOTTOM_PADDING_ = 18;
134 * Number of unregistered destinations that may be promoted to the top.
139 DestinationSearch.MAX_PROMOTED_UNREGISTERED_PRINTERS_ = 2;
141 DestinationSearch.prototype = {
142 __proto__: print_preview.Overlay.prototype,
145 onSetVisibleInternal: function(isVisible) {
147 this.searchBox_.focus();
148 if (getIsVisible(this.getChildElement('.cloudprint-promo'))) {
149 this.metrics_.record(
150 print_preview.Metrics.DestinationSearchBucket.SIGNIN_PROMPT);
152 if (this.userInfo_.initialized)
153 this.onUsersChanged_();
155 this.metrics_.record(
156 print_preview.Metrics.DestinationSearchBucket.DESTINATION_SHOWN);
158 this.destinationStore_.startLoadAllDestinations();
159 this.invitationStore_.startLoadingInvitations();
161 // Collapse all destination lists
162 this.localList_.setIsShowAll(false);
163 this.cloudList_.setIsShowAll(false);
169 onCancelInternal: function() {
170 this.metrics_.record(print_preview.Metrics.DestinationSearchBucket.
171 DESTINATION_CLOSED_UNCHANGED);
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);
185 enterDocument: function() {
186 print_preview.Overlay.prototype.enterDocument.call(this);
189 this.getChildElement('.account-select'),
191 this.onAccountChange_.bind(this));
194 this.getChildElement('.sign-in'),
196 this.onSignInActivated_.bind(this));
199 this.getChildElement('.invitation-accept-button'),
201 this.onInvitationProcessButtonClick_.bind(this, true /*accept*/));
203 this.getChildElement('.invitation-reject-button'),
205 this.onInvitationProcessButtonClick_.bind(this, false /*accept*/));
208 this.getChildElement('.cloudprint-promo > .close-button'),
210 this.onCloudprintPromoCloseButtonClick_.bind(this));
213 print_preview.SearchBox.EventType.SEARCH,
214 this.onSearch_.bind(this));
217 print_preview.DestinationListItem.EventType.SELECT,
218 this.onDestinationSelect_.bind(this));
221 print_preview.DestinationListItem.EventType.REGISTER_PROMO_CLICKED,
223 this.metrics_.record(print_preview.Metrics.DestinationSearchBucket.
224 REGISTER_PROMO_SELECTED);
228 this.destinationStore_,
229 print_preview.DestinationStore.EventType.DESTINATIONS_INSERTED,
230 this.onDestinationsInserted_.bind(this));
232 this.destinationStore_,
233 print_preview.DestinationStore.EventType.DESTINATION_SELECT,
234 this.onDestinationStoreSelect_.bind(this));
236 this.destinationStore_,
237 print_preview.DestinationStore.EventType.DESTINATION_SEARCH_STARTED,
238 this.updateThrobbers_.bind(this));
240 this.destinationStore_,
241 print_preview.DestinationStore.EventType.DESTINATION_SEARCH_DONE,
242 this.onDestinationSearchDone_.bind(this));
245 this.invitationStore_,
246 print_preview.InvitationStore.EventType.INVITATION_SEARCH_DONE,
247 this.updateInvitations_.bind(this));
249 this.invitationStore_,
250 print_preview.InvitationStore.EventType.INVITATION_PROCESSED,
251 this.updateInvitations_.bind(this));
255 print_preview.DestinationList.EventType.ACTION_LINK_ACTIVATED,
256 this.onManageLocalDestinationsActivated_.bind(this));
259 print_preview.DestinationList.EventType.ACTION_LINK_ACTIVATED,
260 this.onManageCloudDestinationsActivated_.bind(this));
264 print_preview.UserInfo.EventType.USERS_CHANGED,
265 this.onUsersChanged_.bind(this));
267 this.tracker.add(window, 'resize', this.onWindowResize_.bind(this));
269 this.updateThrobbers_();
271 // Render any destinations already in the store.
272 this.renderDestinations_();
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">',
285 this.getChildElement('.account-select-label').textContent =
286 loadTimeData.getString('accountSelectTitle');
290 * @return {number} Height available for destination lists, in pixels.
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;
304 * Filters all destination lists with the given query.
305 * @param {RegExp} query Query to filter destination lists by.
308 filterLists_: function(query) {
309 this.recentList_.updateSearchQuery(query);
310 this.localList_.updateSearchQuery(query);
311 this.cloudList_.updateSearchQuery(query);
315 * Resets the search query.
318 resetSearch_: function() {
319 this.searchBox_.setQuery(null);
320 this.filterLists_(null);
324 * Renders all of the destinations in the destination store.
327 renderDestinations_: function() {
328 var recentDestinations = [];
329 var localDestinations = [];
330 var cloudDestinations = [];
331 var unregisteredCloudDestinations = [];
334 this.destinationStore_.destinations(this.userInfo_.activeUser);
335 destinations.forEach(function(destination) {
336 if (destination.isRecent) {
337 recentDestinations.push(destination);
339 if (destination.isLocal ||
340 destination.origin == print_preview.Destination.Origin.DEVICE) {
341 localDestinations.push(destination);
343 if (destination.connectionStatus ==
344 print_preview.Destination.ConnectionStatus.UNREGISTERED) {
345 unregisteredCloudDestinations.push(destination);
347 cloudDestinations.push(destination);
352 if (unregisteredCloudDestinations.length != 0 &&
353 !this.registerPromoShownMetricRecorded_) {
354 this.metrics_.record(
355 print_preview.Metrics.DestinationSearchBucket.REGISTER_PROMO_SHOWN);
356 this.registerPromoShownMetricRecorded_ = true;
359 var finalCloudDestinations = unregisteredCloudDestinations.slice(
360 0, DestinationSearch.MAX_PROMOTED_UNREGISTERED_PRINTERS_).concat(
362 unregisteredCloudDestinations.slice(
363 DestinationSearch.MAX_PROMOTED_UNREGISTERED_PRINTERS_));
365 this.recentList_.updateDestinations(recentDestinations);
366 this.localList_.updateDestinations(localDestinations);
367 this.cloudList_.updateDestinations(finalCloudDestinations);
371 * Reflows the destination lists according to the available height.
374 reflowLists_: function() {
375 if (!this.getIsVisible()) {
379 var hasCloudList = getIsVisible(this.getChildElement('.cloud-list'));
380 var lists = [this.recentList_, this.localList_];
382 lists.push(this.cloudList_);
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_;
391 var getCounts = function(lists, count) {
392 return lists.map(function(list) { return count; });
395 var availableHeight = this.getAvailableListsHeight_();
396 var listsEl = this.getChildElement('.lists');
397 listsEl.style.maxHeight = availableHeight + 'px';
399 var maxListLength = lists.reduce(function(prevCount, list) {
400 return Math.max(prevCount, list.getDestinationsCount());
402 for (var i = 1; i <= maxListLength; i++) {
403 if (getListsTotalHeight(lists, getCounts(lists, i)) > availableHeight) {
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]--;
420 lists.forEach(function(list, index) {
421 list.updateShortListSize(counts[index]);
424 // Set height of the list manually so that search filter doesn't change
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;
434 listsEl.style.height = listsHeight;
439 * Updates whether the throbbers for the various destination lists should be
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);
455 * Updates printer sharing invitations UI.
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);
466 this.invitation_ = invitations[0];
467 this.showInvitation_(this.invitation_);
469 this.invitation_ = null;
472 this.getChildElement('.invitation-container'), !!this.invitation_);
477 * @param {!printe_preview.Invitation} invitation Invitation to show.
480 showInvitation_: function(invitation) {
481 var invitationText = '';
482 if (invitation.asGroupManager) {
483 invitationText = loadTimeData.getStringF(
484 'groupPrinterSharingInviteText',
486 invitation.destination.displayName,
487 invitation.receiver);
489 invitationText = loadTimeData.getStringF(
490 'printerSharingInviteText',
492 invitation.destination.displayName);
494 this.getChildElement('.invitation-text').innerHTML = invitationText;
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;
503 this.getChildElement('#invitation-process-throbber'),
504 !!this.invitationStore_.invitationInProgress);
508 * Called when user's logged in accounts change. Updates the UI.
511 onUsersChanged_: function() {
512 var loggedIn = this.userInfo_.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);
522 var option = document.createElement('option');
523 option.text = loadTimeData.getString('addAccountTitle');
525 accountSelectEl.add(option);
527 accountSelectEl.selectedIndex =
528 this.userInfo_.users.indexOf(this.userInfo_.activeUser);
531 setIsVisible(this.getChildElement('.user-info'), loggedIn);
532 setIsVisible(this.getChildElement('.cloud-list'), loggedIn);
533 setIsVisible(this.getChildElement('.cloudprint-promo'), !loggedIn);
534 this.updateInvitations_();
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.
543 onSearch_: function(evt) {
544 this.filterLists_(evt.queryRegExp);
548 * Called when a destination is selected. Clears the search and hides the
550 * @param {Event} evt Contains the selected destination.
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);
561 * Called when a destination is selected. Selected destination are marked as
562 * recent, so we have to update our recent destinations list.
565 onDestinationStoreSelect_: function() {
567 this.destinationStore_.destinations(this.userInfo_.activeUser);
568 var recentDestinations = [];
569 destinations.forEach(function(destination) {
570 if (destination.isRecent) {
571 recentDestinations.push(destination);
574 this.recentList_.updateDestinations(recentDestinations);
579 * Called when destinations are inserted into the store. Rerenders
583 onDestinationsInserted_: function() {
584 this.renderDestinations_();
589 * Called when destinations are inserted into the store. Rerenders
593 onDestinationSearchDone_: function() {
594 this.updateThrobbers_();
595 this.renderDestinations_();
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();
603 * Called when the manage cloud printers action is activated.
606 onManageCloudDestinationsActivated_: function() {
607 cr.dispatchSimpleEvent(
609 print_preview.DestinationSearch.EventType.MANAGE_CLOUD_DESTINATIONS);
613 * Called when the manage local printers action is activated.
616 onManageLocalDestinationsActivated_: function() {
617 cr.dispatchSimpleEvent(
619 print_preview.DestinationSearch.EventType.MANAGE_LOCAL_DESTINATIONS);
623 * Called when the "Sign in" link on the Google Cloud Print promo is
627 onSignInActivated_: function() {
628 cr.dispatchSimpleEvent(this, DestinationSearch.EventType.SIGN_IN);
629 this.metrics_.record(
630 print_preview.Metrics.DestinationSearchBucket.SIGNIN_TRIGGERED);
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.
638 onAccountChange_: function() {
639 var accountSelectEl = this.getChildElement('.account-select');
641 accountSelectEl.options[accountSelectEl.selectedIndex].value;
643 this.userInfo_.activeUser = account;
644 this.destinationStore_.reloadUserCookieBasedDestinations();
645 this.invitationStore_.startLoadingInvitations();
646 this.metrics_.record(
647 print_preview.Metrics.DestinationSearchBucket.ACCOUNT_CHANGED);
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;
657 this.metrics_.record(
658 print_preview.Metrics.DestinationSearchBucket.ADD_ACCOUNT_SELECTED);
663 * Called when the printer sharing invitation Accept/Reject button is
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_();
676 * Called when the close button on the cloud print promo is clicked. Hides
680 onCloudprintPromoCloseButtonClick_: function() {
681 setIsVisible(this.getChildElement('.cloudprint-promo'), false);
685 * Called when the window is resized. Reflows layout of destination lists.
688 onWindowResize_: function() {
695 DestinationSearch: DestinationSearch