blob: c16f7e2390570d1d736018f48f8a5a05ab0e010e [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
cr.define('print_preview', function() {
'use strict';
/**
* Component used for searching for a print destination.
* This is a modal dialog that allows the user to search and select a
* destination to print to. When a destination is selected, it is written to
* the destination store.
* @param {!print_preview.DestinationStore} destinationStore Data store
* containing the destinations to search through.
* @param {!print_preview.InvitationStore} invitationStore Data store
* holding printer sharing invitations.
* @param {!print_preview.UserInfo} userInfo Event target that contains
* information about the logged in user.
* @constructor
* @extends {print_preview.Overlay}
*/
function DestinationSearch(destinationStore, invitationStore, userInfo) {
print_preview.Overlay.call(this);
/**
* Data store containing the destinations to search through.
* @type {!print_preview.DestinationStore}
* @private
*/
this.destinationStore_ = destinationStore;
/**
* Data store holding printer sharing invitations.
* @type {!print_preview.DestinationStore}
* @private
*/
this.invitationStore_ = invitationStore;
/**
* Event target that contains information about the logged in user.
* @type {!print_preview.UserInfo}
* @private
*/
this.userInfo_ = userInfo;
/**
* Currently displayed printer sharing invitation.
* @type {print_preview.Invitation}
* @private
*/
this.invitation_ = null;
/**
* Used to record usage statistics.
* @type {!print_preview.DestinationSearchMetricsContext}
* @private
*/
this.metrics_ = new print_preview.DestinationSearchMetricsContext();
/**
* Whether or not a UMA histogram for the register promo being shown was
* already recorded.
* @type {boolean}
* @private
*/
this.registerPromoShownMetricRecorded_ = false;
/**
* Search box used to search through the destination lists.
* @type {!print_preview.SearchBox}
* @private
*/
this.searchBox_ = new print_preview.SearchBox(
loadTimeData.getString('searchBoxPlaceholder'));
this.addChild(this.searchBox_);
/**
* Destination list containing recent destinations.
* @type {!print_preview.DestinationList}
* @private
*/
this.recentList_ = new print_preview.RecentDestinationList(this);
this.addChild(this.recentList_);
/**
* Destination list containing local destinations.
* @type {!print_preview.DestinationList}
* @private
*/
this.localList_ = new print_preview.DestinationList(
this,
loadTimeData.getString('localDestinationsTitle'),
cr.isChromeOS ? null : loadTimeData.getString('manage'));
this.addChild(this.localList_);
/**
* Destination list containing cloud destinations.
* @type {!print_preview.DestinationList}
* @private
*/
this.cloudList_ = new print_preview.CloudDestinationList(this);
this.addChild(this.cloudList_);
};
/**
* Event types dispatched by the component.
* @enum {string}
*/
DestinationSearch.EventType = {
// Dispatched when user requests to sign-in into another Google account.
ADD_ACCOUNT: 'print_preview.DestinationSearch.ADD_ACCOUNT',
// Dispatched when the user requests to manage their cloud destinations.
MANAGE_CLOUD_DESTINATIONS:
'print_preview.DestinationSearch.MANAGE_CLOUD_DESTINATIONS',
// Dispatched when the user requests to manage their local destinations.
MANAGE_LOCAL_DESTINATIONS:
'print_preview.DestinationSearch.MANAGE_LOCAL_DESTINATIONS',
// Dispatched when the user requests to sign-in to their Google account.
SIGN_IN: 'print_preview.DestinationSearch.SIGN_IN'
};
/**
* Padding at the bottom of a destination list in pixels.
* @type {number}
* @const
* @private
*/
DestinationSearch.LIST_BOTTOM_PADDING_ = 18;
/**
* Number of unregistered destinations that may be promoted to the top.
* @type {number}
* @const
* @private
*/
DestinationSearch.MAX_PROMOTED_UNREGISTERED_PRINTERS_ = 2;
DestinationSearch.prototype = {
__proto__: print_preview.Overlay.prototype,
/** @override */
onSetVisibleInternal: function(isVisible) {
if (isVisible) {
this.searchBox_.focus();
if (getIsVisible(this.getChildElement('.cloudprint-promo'))) {
this.metrics_.record(
print_preview.Metrics.DestinationSearchBucket.SIGNIN_PROMPT);
}
if (this.userInfo_.initialized)
this.onUsersChanged_();
this.reflowLists_();
this.metrics_.record(
print_preview.Metrics.DestinationSearchBucket.DESTINATION_SHOWN);
this.destinationStore_.startLoadAllDestinations();
this.invitationStore_.startLoadingInvitations();
} else {
// Collapse all destination lists
this.localList_.setIsShowAll(false);
this.cloudList_.setIsShowAll(false);
this.resetSearch_();
}
},
/** @override */
onCancelInternal: function() {
this.metrics_.record(print_preview.Metrics.DestinationSearchBucket.
DESTINATION_CLOSED_UNCHANGED);
},
/** Shows the Google Cloud Print promotion banner. */
showCloudPrintPromo: function() {
setIsVisible(this.getChildElement('.cloudprint-promo'), true);
if (this.getIsVisible()) {
this.metrics_.record(
print_preview.Metrics.DestinationSearchBucket.SIGNIN_PROMPT);
}
this.reflowLists_();
},
/** @override */
enterDocument: function() {
print_preview.Overlay.prototype.enterDocument.call(this);
this.tracker.add(
this.getChildElement('.account-select'),
'change',
this.onAccountChange_.bind(this));
this.tracker.add(
this.getChildElement('.sign-in'),
'click',
this.onSignInActivated_.bind(this));
this.tracker.add(
this.getChildElement('.invitation-accept-button'),
'click',
this.onInvitationProcessButtonClick_.bind(this, true /*accept*/));
this.tracker.add(
this.getChildElement('.invitation-reject-button'),
'click',
this.onInvitationProcessButtonClick_.bind(this, false /*accept*/));
this.tracker.add(
this.getChildElement('.cloudprint-promo > .close-button'),
'click',
this.onCloudprintPromoCloseButtonClick_.bind(this));
this.tracker.add(
this.searchBox_,
print_preview.SearchBox.EventType.SEARCH,
this.onSearch_.bind(this));
this.tracker.add(
this,
print_preview.DestinationListItem.EventType.SELECT,
this.onDestinationSelect_.bind(this));
this.tracker.add(
this,
print_preview.DestinationListItem.EventType.REGISTER_PROMO_CLICKED,
function() {
this.metrics_.record(print_preview.Metrics.DestinationSearchBucket.
REGISTER_PROMO_SELECTED);
}.bind(this));
this.tracker.add(
this.destinationStore_,
print_preview.DestinationStore.EventType.DESTINATIONS_INSERTED,
this.onDestinationsInserted_.bind(this));
this.tracker.add(
this.destinationStore_,
print_preview.DestinationStore.EventType.DESTINATION_SELECT,
this.onDestinationStoreSelect_.bind(this));
this.tracker.add(
this.destinationStore_,
print_preview.DestinationStore.EventType.DESTINATION_SEARCH_STARTED,
this.updateThrobbers_.bind(this));
this.tracker.add(
this.destinationStore_,
print_preview.DestinationStore.EventType.DESTINATION_SEARCH_DONE,
this.onDestinationSearchDone_.bind(this));
this.tracker.add(
this.invitationStore_,
print_preview.InvitationStore.EventType.INVITATION_SEARCH_DONE,
this.updateInvitations_.bind(this));
this.tracker.add(
this.invitationStore_,
print_preview.InvitationStore.EventType.INVITATION_PROCESSED,
this.updateInvitations_.bind(this));
this.tracker.add(
this.localList_,
print_preview.DestinationList.EventType.ACTION_LINK_ACTIVATED,
this.onManageLocalDestinationsActivated_.bind(this));
this.tracker.add(
this.cloudList_,
print_preview.DestinationList.EventType.ACTION_LINK_ACTIVATED,
this.onManageCloudDestinationsActivated_.bind(this));
this.tracker.add(
this.userInfo_,
print_preview.UserInfo.EventType.USERS_CHANGED,
this.onUsersChanged_.bind(this));
this.tracker.add(window, 'resize', this.onWindowResize_.bind(this));
this.updateThrobbers_();
// Render any destinations already in the store.
this.renderDestinations_();
},
/** @override */
decorateInternal: function() {
this.searchBox_.render(this.getChildElement('.search-box-container'));
this.recentList_.render(this.getChildElement('.recent-list'));
this.localList_.render(this.getChildElement('.local-list'));
this.cloudList_.render(this.getChildElement('.cloud-list'));
this.getChildElement('.promo-text').innerHTML = loadTimeData.getStringF(
'cloudPrintPromotion',
'<a is="action-link" class="sign-in">',
'</a>');
this.getChildElement('.account-select-label').textContent =
loadTimeData.getString('accountSelectTitle');
},
/**
* @return {number} Height available for destination lists, in pixels.
* @private
*/
getAvailableListsHeight_: function() {
var elStyle = window.getComputedStyle(this.getElement());
return this.getElement().offsetHeight -
parseInt(elStyle.getPropertyValue('padding-top'), 10) -
parseInt(elStyle.getPropertyValue('padding-bottom'), 10) -
this.getChildElement('.lists').offsetTop -
this.getChildElement('.invitation-container').offsetHeight -
this.getChildElement('.cloudprint-promo').offsetHeight;
},
/**
* Filters all destination lists with the given query.
* @param {RegExp} query Query to filter destination lists by.
* @private
*/
filterLists_: function(query) {
this.recentList_.updateSearchQuery(query);
this.localList_.updateSearchQuery(query);
this.cloudList_.updateSearchQuery(query);
},
/**
* Resets the search query.
* @private
*/
resetSearch_: function() {
this.searchBox_.setQuery(null);
this.filterLists_(null);
},
/**
* Renders all of the destinations in the destination store.
* @private
*/
renderDestinations_: function() {
var recentDestinations = [];
var localDestinations = [];
var cloudDestinations = [];
var unregisteredCloudDestinations = [];
var destinations =
this.destinationStore_.destinations(this.userInfo_.activeUser);
destinations.forEach(function(destination) {
if (destination.isRecent) {
recentDestinations.push(destination);
}
if (destination.isLocal) {
localDestinations.push(destination);
} else {
if (destination.connectionStatus ==
print_preview.Destination.ConnectionStatus.UNREGISTERED) {
unregisteredCloudDestinations.push(destination);
} else {
cloudDestinations.push(destination);
}
}
});
if (unregisteredCloudDestinations.length != 0 &&
!this.registerPromoShownMetricRecorded_) {
this.metrics_.record(
print_preview.Metrics.DestinationSearchBucket.REGISTER_PROMO_SHOWN);
this.registerPromoShownMetricRecorded_ = true;
}
var finalCloudDestinations = unregisteredCloudDestinations.slice(
0, DestinationSearch.MAX_PROMOTED_UNREGISTERED_PRINTERS_).concat(
cloudDestinations,
unregisteredCloudDestinations.slice(
DestinationSearch.MAX_PROMOTED_UNREGISTERED_PRINTERS_));
this.recentList_.updateDestinations(recentDestinations);
this.localList_.updateDestinations(localDestinations);
this.cloudList_.updateDestinations(finalCloudDestinations);
},
/**
* Reflows the destination lists according to the available height.
* @private
*/
reflowLists_: function() {
if (!this.getIsVisible()) {
return;
}
var hasCloudList = getIsVisible(this.getChildElement('.cloud-list'));
var lists = [this.recentList_, this.localList_];
if (hasCloudList) {
lists.push(this.cloudList_);
}
var getListsTotalHeight = function(lists, counts) {
return lists.reduce(function(sum, list, index) {
return sum + list.getEstimatedHeightInPixels(counts[index]) +
DestinationSearch.LIST_BOTTOM_PADDING_;
}, 0);
};
var getCounts = function(lists, count) {
return lists.map(function(list) { return count; });
};
var availableHeight = this.getAvailableListsHeight_();
var listsEl = this.getChildElement('.lists');
listsEl.style.maxHeight = availableHeight + 'px';
var maxListLength = lists.reduce(function(prevCount, list) {
return Math.max(prevCount, list.getDestinationsCount());
}, 0);
for (var i = 1; i <= maxListLength; i++) {
if (getListsTotalHeight(lists, getCounts(lists, i)) > availableHeight) {
i--;
break;
}
}
var counts = getCounts(lists, i);
// Fill up the possible n-1 free slots left by the previous loop.
if (getListsTotalHeight(lists, counts) < availableHeight) {
for (var countIndex = 0; countIndex < counts.length; countIndex++) {
counts[countIndex]++;
if (getListsTotalHeight(lists, counts) > availableHeight) {
counts[countIndex]--;
break;
}
}
}
lists.forEach(function(list, index) {
list.updateShortListSize(counts[index]);
});
// Set height of the list manually so that search filter doesn't change
// lists height.
var listsHeight = getListsTotalHeight(lists, counts) + 'px';
if (listsHeight != listsEl.style.height) {
// Try to close account select if there's a possibility it's open now.
var accountSelectEl = this.getChildElement('.account-select');
if (!accountSelectEl.disabled) {
accountSelectEl.disabled = true;
accountSelectEl.disabled = false;
}
listsEl.style.height = listsHeight;
}
},
/**
* Updates whether the throbbers for the various destination lists should be
* shown or hidden.
* @private
*/
updateThrobbers_: function() {
this.localList_.setIsThrobberVisible(
this.destinationStore_.isLocalDestinationSearchInProgress);
this.cloudList_.setIsThrobberVisible(
this.destinationStore_.isCloudDestinationSearchInProgress);
this.recentList_.setIsThrobberVisible(
this.destinationStore_.isLocalDestinationSearchInProgress &&
this.destinationStore_.isCloudDestinationSearchInProgress);
this.reflowLists_();
},
/**
* Updates printer sharing invitations UI.
* @private
*/
updateInvitations_: function() {
var invitations = this.userInfo_.activeUser ?
this.invitationStore_.invitations(this.userInfo_.activeUser) : [];
if (invitations.length > 0) {
if (this.invitation_ != invitations[0]) {
this.metrics_.record(print_preview.Metrics.DestinationSearchBucket.
INVITATION_AVAILABLE);
}
this.invitation_ = invitations[0];
this.showInvitation_(this.invitation_);
} else {
this.invitation_ = null;
}
setIsVisible(
this.getChildElement('.invitation-container'), !!this.invitation_);
this.reflowLists_();
},
/**
* @param {!printe_preview.Invitation} invitation Invitation to show.
* @private
*/
showInvitation_: function(invitation) {
var invitationText = '';
if (invitation.asGroupManager) {
invitationText = loadTimeData.getStringF(
'groupPrinterSharingInviteText',
invitation.sender,
invitation.destination.displayName,
invitation.receiver);
} else {
invitationText = loadTimeData.getStringF(
'printerSharingInviteText',
invitation.sender,
invitation.destination.displayName);
}
this.getChildElement('.invitation-text').innerHTML = invitationText;
var acceptButton = this.getChildElement('.invitation-accept-button');
acceptButton.textContent = loadTimeData.getString(
invitation.asGroupManager ? 'acceptForGroup' : 'accept');
acceptButton.disabled = !!this.invitationStore_.invitationInProgress;
this.getChildElement('.invitation-reject-button').disabled =
!!this.invitationStore_.invitationInProgress;
setIsVisible(
this.getChildElement('#invitation-process-throbber'),
!!this.invitationStore_.invitationInProgress);
},
/**
* Called when user's logged in accounts change. Updates the UI.
* @private
*/
onUsersChanged_: function() {
var loggedIn = this.userInfo_.loggedIn;
if (loggedIn) {
var accountSelectEl = this.getChildElement('.account-select');
accountSelectEl.innerHTML = '';
this.userInfo_.users.forEach(function(account) {
var option = document.createElement('option');
option.text = account;
option.value = account;
accountSelectEl.add(option);
});
var option = document.createElement('option');
option.text = loadTimeData.getString('addAccountTitle');
option.value = '';
accountSelectEl.add(option);
accountSelectEl.selectedIndex =
this.userInfo_.users.indexOf(this.userInfo_.activeUser);
}
setIsVisible(this.getChildElement('.user-info'), loggedIn);
setIsVisible(this.getChildElement('.cloud-list'), loggedIn);
setIsVisible(this.getChildElement('.cloudprint-promo'), !loggedIn);
this.updateInvitations_();
},
/**
* Called when a destination search should be executed. Filters the
* destination lists with the given query.
* @param {Event} evt Contains the search query.
* @private
*/
onSearch_: function(evt) {
this.filterLists_(evt.queryRegExp);
},
/**
* Called when a destination is selected. Clears the search and hides the
* widget.
* @param {Event} evt Contains the selected destination.
* @private
*/
onDestinationSelect_: function(evt) {
this.setIsVisible(false);
this.destinationStore_.selectDestination(evt.destination);
this.metrics_.record(print_preview.Metrics.DestinationSearchBucket.
DESTINATION_CLOSED_CHANGED);
},
/**
* Called when a destination is selected. Selected destination are marked as
* recent, so we have to update our recent destinations list.
* @private
*/
onDestinationStoreSelect_: function() {
var destinations =
this.destinationStore_.destinations(this.userInfo_.activeUser);
var recentDestinations = [];
destinations.forEach(function(destination) {
if (destination.isRecent) {
recentDestinations.push(destination);
}
});
this.recentList_.updateDestinations(recentDestinations);
this.reflowLists_();
},
/**
* Called when destinations are inserted into the store. Rerenders
* destinations.
* @private
*/
onDestinationsInserted_: function() {
this.renderDestinations_();
this.reflowLists_();
},
/**
* Called when destinations are inserted into the store. Rerenders
* destinations.
* @private
*/
onDestinationSearchDone_: function() {
this.updateThrobbers_();
this.renderDestinations_();
this.reflowLists_();
// In case user account information was retrieved with this search
// (knowing current user account is required to fetch invitations).
this.invitationStore_.startLoadingInvitations();
},
/**
* Called when the manage cloud printers action is activated.
* @private
*/
onManageCloudDestinationsActivated_: function() {
cr.dispatchSimpleEvent(
this,
print_preview.DestinationSearch.EventType.MANAGE_CLOUD_DESTINATIONS);
},
/**
* Called when the manage local printers action is activated.
* @private
*/
onManageLocalDestinationsActivated_: function() {
cr.dispatchSimpleEvent(
this,
print_preview.DestinationSearch.EventType.MANAGE_LOCAL_DESTINATIONS);
},
/**
* Called when the "Sign in" link on the Google Cloud Print promo is
* activated.
* @private
*/
onSignInActivated_: function() {
cr.dispatchSimpleEvent(this, DestinationSearch.EventType.SIGN_IN);
this.metrics_.record(
print_preview.Metrics.DestinationSearchBucket.SIGNIN_TRIGGERED);
},
/**
* Called when item in the Accounts list is selected. Initiates active user
* switch or, for 'Add account...' item, opens Google sign-in page.
* @private
*/
onAccountChange_: function() {
var accountSelectEl = this.getChildElement('.account-select');
var account =
accountSelectEl.options[accountSelectEl.selectedIndex].value;
if (account) {
this.userInfo_.activeUser = account;
this.destinationStore_.reloadUserCookieBasedDestinations();
this.invitationStore_.startLoadingInvitations();
this.metrics_.record(
print_preview.Metrics.DestinationSearchBucket.ACCOUNT_CHANGED);
} else {
cr.dispatchSimpleEvent(this, DestinationSearch.EventType.ADD_ACCOUNT);
// Set selection back to the active user.
for (var i = 0; i < accountSelectEl.options.length; i++) {
if (accountSelectEl.options[i].value == this.userInfo_.activeUser) {
accountSelectEl.selectedIndex = i;
break;
}
}
this.metrics_.record(
print_preview.Metrics.DestinationSearchBucket.ADD_ACCOUNT_SELECTED);
}
},
/**
* Called when the printer sharing invitation Accept/Reject button is
* clicked.
* @private
*/
onInvitationProcessButtonClick_: function(accept) {
this.metrics_.record(accept ?
print_preview.Metrics.DestinationSearchBucket.INVITATION_ACCEPTED :
print_preview.Metrics.DestinationSearchBucket.INVITATION_REJECTED);
this.invitationStore_.processInvitation(this.invitation_, accept);
this.updateInvitations_();
},
/**
* Called when the close button on the cloud print promo is clicked. Hides
* the promo.
* @private
*/
onCloudprintPromoCloseButtonClick_: function() {
setIsVisible(this.getChildElement('.cloudprint-promo'), false);
},
/**
* Called when the window is resized. Reflows layout of destination lists.
* @private
*/
onWindowResize_: function() {
this.reflowLists_();
}
};
// Export
return {
DestinationSearch: DestinationSearch
};
});