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 * Child overlay used for resolving a provisional destination. The overlay
70 * is shown when the user attempts to select a provisional destination.
71 * Set only when a destination is being resolved.
72 * @private {?print_preview.ProvisionalDestinationResolver}
74 this.provisionalDestinationResolver_ = null;
77 * Search box used to search through the destination lists.
78 * @type {!print_preview.SearchBox}
81 this.searchBox_ = new print_preview.SearchBox(
82 loadTimeData.getString('searchBoxPlaceholder'));
83 this.addChild(this.searchBox_);
86 * Destination list containing recent destinations.
87 * @type {!print_preview.DestinationList}
90 this.recentList_ = new print_preview.RecentDestinationList(this);
91 this.addChild(this.recentList_);
94 * Destination list containing local destinations.
95 * @type {!print_preview.DestinationList}
98 this.localList_ = new print_preview.DestinationList(
100 loadTimeData.getString('localDestinationsTitle'),
101 cr.isChromeOS ? null : loadTimeData.getString('manage'));
102 this.addChild(this.localList_);
105 * Destination list containing cloud destinations.
106 * @type {!print_preview.DestinationList}
109 this.cloudList_ = new print_preview.CloudDestinationList(this);
110 this.addChild(this.cloudList_);
114 * Event types dispatched by the component.
117 DestinationSearch.EventType = {
118 // Dispatched when user requests to sign-in into another Google account.
119 ADD_ACCOUNT: 'print_preview.DestinationSearch.ADD_ACCOUNT',
121 // Dispatched when the user requests to manage their cloud destinations.
122 MANAGE_CLOUD_DESTINATIONS:
123 'print_preview.DestinationSearch.MANAGE_CLOUD_DESTINATIONS',
125 // Dispatched when the user requests to manage their local destinations.
126 MANAGE_LOCAL_DESTINATIONS:
127 'print_preview.DestinationSearch.MANAGE_LOCAL_DESTINATIONS',
129 // Dispatched when the user requests to sign-in to their Google account.
130 SIGN_IN: 'print_preview.DestinationSearch.SIGN_IN'
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);
164 if (this.provisionalDestinationResolver_)
165 this.provisionalDestinationResolver_.cancel();
171 onCancelInternal: function() {
172 this.metrics_.record(print_preview.Metrics.DestinationSearchBucket.
173 DESTINATION_CLOSED_UNCHANGED);
176 /** Shows the Google Cloud Print promotion banner. */
177 showCloudPrintPromo: function() {
178 setIsVisible(this.getChildElement('.cloudprint-promo'), true);
179 if (this.getIsVisible()) {
180 this.metrics_.record(
181 print_preview.Metrics.DestinationSearchBucket.SIGNIN_PROMPT);
187 enterDocument: function() {
188 print_preview.Overlay.prototype.enterDocument.call(this);
191 this.getChildElement('.account-select'),
193 this.onAccountChange_.bind(this));
196 this.getChildElement('.sign-in'),
198 this.onSignInActivated_.bind(this));
201 this.getChildElement('.invitation-accept-button'),
203 this.onInvitationProcessButtonClick_.bind(this, true /*accept*/));
205 this.getChildElement('.invitation-reject-button'),
207 this.onInvitationProcessButtonClick_.bind(this, false /*accept*/));
210 this.getChildElement('.cloudprint-promo > .close-button'),
212 this.onCloudprintPromoCloseButtonClick_.bind(this));
215 print_preview.SearchBox.EventType.SEARCH,
216 this.onSearch_.bind(this));
219 print_preview.DestinationListItem.EventType.SELECT,
220 this.onDestinationSelect_.bind(this));
223 print_preview.DestinationListItem.EventType.REGISTER_PROMO_CLICKED,
225 this.metrics_.record(print_preview.Metrics.DestinationSearchBucket.
226 REGISTER_PROMO_SELECTED);
230 this.destinationStore_,
231 print_preview.DestinationStore.EventType.DESTINATIONS_INSERTED,
232 this.onDestinationsInserted_.bind(this));
234 this.destinationStore_,
235 print_preview.DestinationStore.EventType.DESTINATION_SELECT,
236 this.onDestinationStoreSelect_.bind(this));
238 this.destinationStore_,
239 print_preview.DestinationStore.EventType.DESTINATION_SEARCH_STARTED,
240 this.updateThrobbers_.bind(this));
242 this.destinationStore_,
243 print_preview.DestinationStore.EventType.DESTINATION_SEARCH_DONE,
244 this.onDestinationSearchDone_.bind(this));
246 this.destinationStore_,
247 print_preview.DestinationStore.EventType
248 .PROVISIONAL_DESTINATION_RESOLVED,
249 this.onDestinationsInserted_.bind(this));
252 this.invitationStore_,
253 print_preview.InvitationStore.EventType.INVITATION_SEARCH_DONE,
254 this.updateInvitations_.bind(this));
256 this.invitationStore_,
257 print_preview.InvitationStore.EventType.INVITATION_PROCESSED,
258 this.updateInvitations_.bind(this));
262 print_preview.DestinationList.EventType.ACTION_LINK_ACTIVATED,
263 this.onManageLocalDestinationsActivated_.bind(this));
266 print_preview.DestinationList.EventType.ACTION_LINK_ACTIVATED,
267 this.onManageCloudDestinationsActivated_.bind(this));
271 print_preview.UserInfo.EventType.USERS_CHANGED,
272 this.onUsersChanged_.bind(this));
275 this.getChildElement('.button-strip .cancel-button'),
277 this.cancel.bind(this));
279 this.tracker.add(window, 'resize', this.onWindowResize_.bind(this));
281 this.updateThrobbers_();
283 // Render any destinations already in the store.
284 this.renderDestinations_();
288 decorateInternal: function() {
289 this.searchBox_.render(this.getChildElement('.search-box-container'));
290 this.recentList_.render(this.getChildElement('.recent-list'));
291 this.localList_.render(this.getChildElement('.local-list'));
292 this.cloudList_.render(this.getChildElement('.cloud-list'));
293 this.getChildElement('.promo-text').innerHTML = loadTimeData.getStringF(
294 'cloudPrintPromotion',
295 '<a is="action-link" class="sign-in">',
297 this.getChildElement('.account-select-label').textContent =
298 loadTimeData.getString('accountSelectTitle');
302 * @return {number} Height available for destination lists, in pixels.
305 getAvailableListsHeight_: function() {
306 var elStyle = window.getComputedStyle(this.getElement());
307 return this.getElement().offsetHeight -
308 parseInt(elStyle.getPropertyValue('padding-top'), 10) -
309 parseInt(elStyle.getPropertyValue('padding-bottom'), 10) -
310 this.getChildElement('.lists').offsetTop -
311 this.getChildElement('.invitation-container').offsetHeight -
312 this.getChildElement('.cloudprint-promo').offsetHeight -
313 this.getChildElement('.action-area').offsetHeight;
317 * Filters all destination lists with the given query.
318 * @param {RegExp} query Query to filter destination lists by.
321 filterLists_: function(query) {
322 this.recentList_.updateSearchQuery(query);
323 this.localList_.updateSearchQuery(query);
324 this.cloudList_.updateSearchQuery(query);
328 * Resets the search query.
331 resetSearch_: function() {
332 this.searchBox_.setQuery(null);
333 this.filterLists_(null);
337 * Renders all of the destinations in the destination store.
340 renderDestinations_: function() {
341 var recentDestinations = [];
342 var localDestinations = [];
343 var cloudDestinations = [];
344 var unregisteredCloudDestinations = [];
347 this.destinationStore_.destinations(this.userInfo_.activeUser);
348 destinations.forEach(function(destination) {
349 if (destination.isRecent) {
350 recentDestinations.push(destination);
352 if (destination.isLocal ||
353 destination.origin == print_preview.Destination.Origin.DEVICE) {
354 localDestinations.push(destination);
356 if (destination.connectionStatus ==
357 print_preview.Destination.ConnectionStatus.UNREGISTERED) {
358 unregisteredCloudDestinations.push(destination);
360 cloudDestinations.push(destination);
365 if (unregisteredCloudDestinations.length != 0 &&
366 !this.registerPromoShownMetricRecorded_) {
367 this.metrics_.record(
368 print_preview.Metrics.DestinationSearchBucket.REGISTER_PROMO_SHOWN);
369 this.registerPromoShownMetricRecorded_ = true;
372 var finalCloudDestinations = unregisteredCloudDestinations.slice(
373 0, DestinationSearch.MAX_PROMOTED_UNREGISTERED_PRINTERS_).concat(
375 unregisteredCloudDestinations.slice(
376 DestinationSearch.MAX_PROMOTED_UNREGISTERED_PRINTERS_));
378 this.recentList_.updateDestinations(recentDestinations);
379 this.localList_.updateDestinations(localDestinations);
380 this.cloudList_.updateDestinations(finalCloudDestinations);
384 * Reflows the destination lists according to the available height.
387 reflowLists_: function() {
388 if (!this.getIsVisible()) {
392 var hasCloudList = getIsVisible(this.getChildElement('.cloud-list'));
393 var lists = [this.recentList_, this.localList_];
395 lists.push(this.cloudList_);
398 var getListsTotalHeight = function(lists, counts) {
399 return lists.reduce(function(sum, list, index) {
400 var container = list.getContainerElement();
401 return sum + list.getEstimatedHeightInPixels(counts[index]) +
402 parseInt(window.getComputedStyle(container).paddingBottom, 10);
405 var getCounts = function(lists, count) {
406 return lists.map(function(list) { return count; });
409 var availableHeight = this.getAvailableListsHeight_();
410 var listsEl = this.getChildElement('.lists');
411 listsEl.style.maxHeight = availableHeight + 'px';
413 var maxListLength = lists.reduce(function(prevCount, list) {
414 return Math.max(prevCount, list.getDestinationsCount());
416 for (var i = 1; i <= maxListLength; i++) {
417 if (getListsTotalHeight(lists, getCounts(lists, i)) > availableHeight) {
422 var counts = getCounts(lists, i);
423 // Fill up the possible n-1 free slots left by the previous loop.
424 if (getListsTotalHeight(lists, counts) < availableHeight) {
425 for (var countIndex = 0; countIndex < counts.length; countIndex++) {
426 counts[countIndex]++;
427 if (getListsTotalHeight(lists, counts) > availableHeight) {
428 counts[countIndex]--;
434 lists.forEach(function(list, index) {
435 list.updateShortListSize(counts[index]);
438 // Set height of the list manually so that search filter doesn't change
440 var listsHeight = getListsTotalHeight(lists, counts) + 'px';
441 if (listsHeight != listsEl.style.height) {
442 // Try to close account select if there's a possibility it's open now.
443 var accountSelectEl = this.getChildElement('.account-select');
444 if (!accountSelectEl.disabled) {
445 accountSelectEl.disabled = true;
446 accountSelectEl.disabled = false;
448 listsEl.style.height = listsHeight;
453 * Updates whether the throbbers for the various destination lists should be
457 updateThrobbers_: function() {
458 this.localList_.setIsThrobberVisible(
459 this.destinationStore_.isLocalDestinationSearchInProgress);
460 this.cloudList_.setIsThrobberVisible(
461 this.destinationStore_.isCloudDestinationSearchInProgress);
462 this.recentList_.setIsThrobberVisible(
463 this.destinationStore_.isLocalDestinationSearchInProgress &&
464 this.destinationStore_.isCloudDestinationSearchInProgress);
469 * Updates printer sharing invitations UI.
472 updateInvitations_: function() {
473 var invitations = this.userInfo_.activeUser ?
474 this.invitationStore_.invitations(this.userInfo_.activeUser) : [];
475 if (invitations.length > 0) {
476 if (this.invitation_ != invitations[0]) {
477 this.metrics_.record(print_preview.Metrics.DestinationSearchBucket.
478 INVITATION_AVAILABLE);
480 this.invitation_ = invitations[0];
481 this.showInvitation_(this.invitation_);
483 this.invitation_ = null;
486 this.getChildElement('.invitation-container'), !!this.invitation_);
491 * @param {!printe_preview.Invitation} invitation Invitation to show.
494 showInvitation_: function(invitation) {
495 var invitationText = '';
496 if (invitation.asGroupManager) {
497 invitationText = loadTimeData.getStringF(
498 'groupPrinterSharingInviteText',
499 HTMLEscape(invitation.sender),
500 HTMLEscape(invitation.destination.displayName),
501 HTMLEscape(invitation.receiver));
503 invitationText = loadTimeData.getStringF(
504 'printerSharingInviteText',
505 HTMLEscape(invitation.sender),
506 HTMLEscape(invitation.destination.displayName));
508 this.getChildElement('.invitation-text').innerHTML = invitationText;
510 var acceptButton = this.getChildElement('.invitation-accept-button');
511 acceptButton.textContent = loadTimeData.getString(
512 invitation.asGroupManager ? 'acceptForGroup' : 'accept');
513 acceptButton.disabled = !!this.invitationStore_.invitationInProgress;
514 this.getChildElement('.invitation-reject-button').disabled =
515 !!this.invitationStore_.invitationInProgress;
517 this.getChildElement('#invitation-process-throbber'),
518 !!this.invitationStore_.invitationInProgress);
522 * Called when user's logged in accounts change. Updates the UI.
525 onUsersChanged_: function() {
526 var loggedIn = this.userInfo_.loggedIn;
528 var accountSelectEl = this.getChildElement('.account-select');
529 accountSelectEl.innerHTML = '';
530 this.userInfo_.users.forEach(function(account) {
531 var option = document.createElement('option');
532 option.text = account;
533 option.value = account;
534 accountSelectEl.add(option);
536 var option = document.createElement('option');
537 option.text = loadTimeData.getString('addAccountTitle');
539 accountSelectEl.add(option);
541 accountSelectEl.selectedIndex =
542 this.userInfo_.users.indexOf(this.userInfo_.activeUser);
545 setIsVisible(this.getChildElement('.user-info'), loggedIn);
546 setIsVisible(this.getChildElement('.cloud-list'), loggedIn);
547 setIsVisible(this.getChildElement('.cloudprint-promo'), !loggedIn);
548 this.updateInvitations_();
552 * Called when a destination search should be executed. Filters the
553 * destination lists with the given query.
554 * @param {Event} evt Contains the search query.
557 onSearch_: function(evt) {
558 this.filterLists_(evt.queryRegExp);
562 * Handler for {@code print_preview.DestinationListItem.EventType.SELECT}
563 * event, which is called when a destinationi list item is selected.
564 * @param {Event} evt Contains the selected destination.
567 onDestinationSelect_: function(evt) {
568 this.handleOnDestinationSelect_(evt.destination);
572 * Called when a destination is selected. Clears the search and hides the
573 * widget. If The destination is provisional, it runs provisional
574 * destination resolver first.
575 * @param {!print_preview.Destination} destination The selected destination.
578 handleOnDestinationSelect_: function(destination) {
579 if (destination.isProvisional) {
580 assert(!this.provisionalDestinationResolver_,
581 'Provisional destination resolver already exists.');
582 this.provisionalDestinationResolver_ =
583 print_preview.ProvisionalDestinationResolver.create(
584 this.destinationStore_, destination);
585 assert(!!this.provisionalDestinationResolver_,
586 'Unable to create provisional destination resolver');
588 var lastFocusedElement = document.activeElement;
589 this.addChild(this.provisionalDestinationResolver_);
590 this.provisionalDestinationResolver_.run(this.getElement())
593 * @param {!print_preview.Destination} resolvedDestination
594 * Destination to which the provisional destination was
597 function(resolvedDestination) {
598 this.handleOnDestinationSelect_(resolvedDestination);
602 console.log('Failed to resolve provisional destination: ' +
607 this.removeChild(this.provisionalDestinationResolver_);
608 this.provisionalDestinationResolver_ = null;
610 // Restore focus to the previosly focused element if it's
611 // still shown in the search.
612 if (lastFocusedElement &&
613 this.getIsVisible() &&
614 getIsVisible(lastFocusedElement) &&
615 this.getElement().contains(lastFocusedElement)) {
616 lastFocusedElement.focus();
622 this.setIsVisible(false);
623 this.destinationStore_.selectDestination(destination);
624 this.metrics_.record(print_preview.Metrics.DestinationSearchBucket.
625 DESTINATION_CLOSED_CHANGED);
629 * Called when a destination is selected. Selected destination are marked as
630 * recent, so we have to update our recent destinations list.
633 onDestinationStoreSelect_: function() {
635 this.destinationStore_.destinations(this.userInfo_.activeUser);
636 var recentDestinations = [];
637 destinations.forEach(function(destination) {
638 if (destination.isRecent) {
639 recentDestinations.push(destination);
642 this.recentList_.updateDestinations(recentDestinations);
647 * Called when destinations are inserted into the store. Rerenders
651 onDestinationsInserted_: function() {
652 this.renderDestinations_();
657 * Called when destinations are inserted into the store. Rerenders
661 onDestinationSearchDone_: function() {
662 this.updateThrobbers_();
663 this.renderDestinations_();
665 // In case user account information was retrieved with this search
666 // (knowing current user account is required to fetch invitations).
667 this.invitationStore_.startLoadingInvitations();
671 * Called when the manage cloud printers action is activated.
674 onManageCloudDestinationsActivated_: function() {
675 cr.dispatchSimpleEvent(
677 print_preview.DestinationSearch.EventType.MANAGE_CLOUD_DESTINATIONS);
681 * Called when the manage local printers action is activated.
684 onManageLocalDestinationsActivated_: function() {
685 cr.dispatchSimpleEvent(
687 print_preview.DestinationSearch.EventType.MANAGE_LOCAL_DESTINATIONS);
691 * Called when the "Sign in" link on the Google Cloud Print promo is
695 onSignInActivated_: function() {
696 cr.dispatchSimpleEvent(this, DestinationSearch.EventType.SIGN_IN);
697 this.metrics_.record(
698 print_preview.Metrics.DestinationSearchBucket.SIGNIN_TRIGGERED);
702 * Called when item in the Accounts list is selected. Initiates active user
703 * switch or, for 'Add account...' item, opens Google sign-in page.
706 onAccountChange_: function() {
707 var accountSelectEl = this.getChildElement('.account-select');
709 accountSelectEl.options[accountSelectEl.selectedIndex].value;
711 this.userInfo_.activeUser = account;
712 this.destinationStore_.reloadUserCookieBasedDestinations();
713 this.invitationStore_.startLoadingInvitations();
714 this.metrics_.record(
715 print_preview.Metrics.DestinationSearchBucket.ACCOUNT_CHANGED);
717 cr.dispatchSimpleEvent(this, DestinationSearch.EventType.ADD_ACCOUNT);
718 // Set selection back to the active user.
719 for (var i = 0; i < accountSelectEl.options.length; i++) {
720 if (accountSelectEl.options[i].value == this.userInfo_.activeUser) {
721 accountSelectEl.selectedIndex = i;
725 this.metrics_.record(
726 print_preview.Metrics.DestinationSearchBucket.ADD_ACCOUNT_SELECTED);
731 * Called when the printer sharing invitation Accept/Reject button is
735 onInvitationProcessButtonClick_: function(accept) {
736 this.metrics_.record(accept ?
737 print_preview.Metrics.DestinationSearchBucket.INVITATION_ACCEPTED :
738 print_preview.Metrics.DestinationSearchBucket.INVITATION_REJECTED);
739 this.invitationStore_.processInvitation(this.invitation_, accept);
740 this.updateInvitations_();
744 * Called when the close button on the cloud print promo is clicked. Hides
748 onCloudprintPromoCloseButtonClick_: function() {
749 setIsVisible(this.getChildElement('.cloudprint-promo'), false);
754 * Called when the window is resized. Reflows layout of destination lists.
757 onWindowResize_: function() {
764 DestinationSearch: DestinationSearch