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