зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1471391 - Create initial CFR doorhanger r=k88hudson
Differential Revision: https://phabricator.services.mozilla.com/D4266 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
8918ef8375
Коммит
1991678183
|
@ -19,6 +19,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
|
|||
BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.jsm",
|
||||
BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
|
||||
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
|
||||
CFRPageActions: "resource://activity-stream/lib/CFRPageActions.jsm",
|
||||
CharsetMenu: "resource://gre/modules/CharsetMenu.jsm",
|
||||
Color: "resource://gre/modules/Color.jsm",
|
||||
ContentSearch: "resource:///modules/ContentSearch.jsm",
|
||||
|
@ -4817,6 +4818,8 @@ var XULBrowserWindow = {
|
|||
CustomizationHandler.isCustomizing()) {
|
||||
gCustomizeMode.exit();
|
||||
}
|
||||
|
||||
CFRPageActions.updatePageActions(gBrowser.selectedBrowser);
|
||||
}
|
||||
UpdateBackForwardCommands(gBrowser.webNavigation);
|
||||
ReaderParent.updateReaderButton(gBrowser.selectedBrowser);
|
||||
|
|
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="context-fill" d="M14.5 8c-.971 0-1 1-1.75 1a.765.765 0 0 1-.75-.75V5a1 1 0 0 0-1-1H7.75A.765.765 0 0 1 7 3.25c0-.75 1-.779 1-1.75C8 .635 7.1 0 6 0S4 .635 4 1.5c0 .971 1 1 1 1.75a.765.765 0 0 1-.75.75H1a1 1 0 0 0-1 1v2.25A.765.765 0 0 0 .75 8c.75 0 .779-1 1.75-1C3.365 7 4 7.9 4 9s-.635 2-1.5 2c-.971 0-1-1-1.75-1a.765.765 0 0 0-.75.75V15a1 1 0 0 0 1 1h3.25a.765.765 0 0 0 .75-.75c0-.75-1-.779-1-1.75 0-.865.9-1.5 2-1.5s2 .635 2 1.5c0 .971-1 1-1 1.75a.765.765 0 0 0 .75.75H11a1 1 0 0 0 1-1v-3.25a.765.765 0 0 1 .75-.75c.75 0 .779 1 1.75 1 .865 0 1.5-.9 1.5-2s-.635-2-1.5-2z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="context-fill" fill-opacity="context-fill-opacity" d="M14.5 8c-.971 0-1 1-1.75 1a.765.765 0 0 1-.75-.75V5a1 1 0 0 0-1-1H7.75A.765.765 0 0 1 7 3.25c0-.75 1-.779 1-1.75C8 .635 7.1 0 6 0S4 .635 4 1.5c0 .971 1 1 1 1.75a.765.765 0 0 1-.75.75H1a1 1 0 0 0-1 1v2.25A.765.765 0 0 0 .75 8c.75 0 .779-1 1.75-1C3.365 7 4 7.9 4 9s-.635 2-1.5 2c-.971 0-1-1-1.75-1a.765.765 0 0 0-.75.75V15a1 1 0 0 0 1 1h3.25a.765.765 0 0 0 .75-.75c0-.75-1-.779-1-1.75 0-.865.9-1.5 2-1.5s2 .635 2 1.5c0 .971-1 1-1 1.75a.765.765 0 0 0 .75.75H11a1 1 0 0 0 1-1v-3.25a.765.765 0 0 1 .75-.75c.75 0 .779 1 1.75 1 .865 0 1.5-.9 1.5-2s-.635-2-1.5-2z"/></svg>
|
До Ширина: | Высота: | Размер: 653 B После Ширина: | Высота: | Размер: 689 B |
|
@ -0,0 +1,247 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const POPUP_NOTIFICATION_ID = "contextual-feature-recommendation";
|
||||
|
||||
const DELAY_BEFORE_EXPAND_MS = 1000;
|
||||
const DURATION_OF_EXPAND_MS = 5000;
|
||||
|
||||
/**
|
||||
* A WeakMap from browsers to {host, recommendation} pairs. Recommendations are
|
||||
* defined in the ExtensionDoorhanger.schema.json.
|
||||
*
|
||||
* A recommendation is specific to a browser and host and is active until the
|
||||
* given browser is closed or the user navigates (within that browser) away from
|
||||
* the host.
|
||||
*/
|
||||
const RecommendationMap = new WeakMap();
|
||||
|
||||
/**
|
||||
* A WeakMap from windows to their CFR PageAction.
|
||||
*/
|
||||
const PageActionMap = new WeakMap();
|
||||
|
||||
/**
|
||||
* We need one PageAction for each window
|
||||
*/
|
||||
class PageAction {
|
||||
constructor(win, dispatchToASRouter) {
|
||||
this.window = win;
|
||||
this.urlbar = win.document.getElementById("urlbar");
|
||||
this.container = win.document.getElementById("contextual-feature-recommendation");
|
||||
this.button = win.document.getElementById("cfr-button");
|
||||
this.label = win.document.getElementById("cfr-label");
|
||||
|
||||
this._dispatchToASRouter = dispatchToASRouter;
|
||||
this._popupStateChange = this._popupStateChange.bind(this);
|
||||
this._collapse = this._collapse.bind(this);
|
||||
this._handleClick = this._handleClick.bind(this);
|
||||
|
||||
// Saved timeout IDs for scheduled state changes, so they can be cancelled
|
||||
this.stateTransitionTimeoutIDs = [];
|
||||
|
||||
this.container.onclick = this._handleClick;
|
||||
}
|
||||
|
||||
async show(notificationText, shouldExpand = false) {
|
||||
this.container.hidden = false;
|
||||
|
||||
this.label.value = notificationText;
|
||||
|
||||
// Wait for layout to flush to avoid a synchronous reflow then calculate the
|
||||
// label width. We can safely get the width even though the recommendation is
|
||||
// collapsed; the label itself remains full width (with its overflow hidden)
|
||||
await this.window.promiseDocumentFlushed;
|
||||
const [{width}] = this.label.getClientRects();
|
||||
this.urlbar.style.setProperty("--cfr-label-width", `${width}px`);
|
||||
|
||||
if (shouldExpand) {
|
||||
this._clearScheduledStateChanges();
|
||||
|
||||
// After one second, expand
|
||||
this._expand(DELAY_BEFORE_EXPAND_MS);
|
||||
|
||||
// Five seconds later, collapse again
|
||||
this._collapse(DELAY_BEFORE_EXPAND_MS + DURATION_OF_EXPAND_MS);
|
||||
}
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.container.hidden = true;
|
||||
this._clearScheduledStateChanges();
|
||||
this.urlbar.removeAttribute("cfr-recommendation-state");
|
||||
}
|
||||
|
||||
_expand(delay = 0) {
|
||||
if (!delay) {
|
||||
// Non-delayed state change overrides any scheduled state changes
|
||||
this._clearScheduledStateChanges();
|
||||
this.urlbar.setAttribute("cfr-recommendation-state", "expanded");
|
||||
} else {
|
||||
this.stateTransitionTimeoutIDs.push(this.window.setTimeout(() => {
|
||||
this.urlbar.setAttribute("cfr-recommendation-state", "expanded");
|
||||
}, delay));
|
||||
}
|
||||
}
|
||||
|
||||
_collapse(delay = 0) {
|
||||
if (!delay) {
|
||||
// Non-delayed state change overrides any scheduled state changes
|
||||
this._clearScheduledStateChanges();
|
||||
if (this.urlbar.getAttribute("cfr-recommendation-state") === "expanded") {
|
||||
this.urlbar.setAttribute("cfr-recommendation-state", "collapsed");
|
||||
}
|
||||
} else {
|
||||
this.stateTransitionTimeoutIDs.push(this.window.setTimeout(() => {
|
||||
if (this.urlbar.getAttribute("cfr-recommendation-state") === "expanded") {
|
||||
this.urlbar.setAttribute("cfr-recommendation-state", "collapsed");
|
||||
}
|
||||
}, delay));
|
||||
}
|
||||
}
|
||||
|
||||
_clearScheduledStateChanges() {
|
||||
while (this.stateTransitionTimeoutIDs.length > 0) {
|
||||
// clearTimeout is safe even with invalid/expired IDs
|
||||
this.window.clearTimeout(this.stateTransitionTimeoutIDs.pop());
|
||||
}
|
||||
}
|
||||
|
||||
// This is called when the popup closes as a result of interaction _outside_
|
||||
// the popup, e.g. by hitting <esc>
|
||||
_popupStateChange(state) {
|
||||
if (["dismissed", "removed"].includes(state)) {
|
||||
this._collapse();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Respond to a user click on the recommendation by showing a doorhanger/
|
||||
* popup notification
|
||||
*/
|
||||
_handleClick(event) {
|
||||
const browser = this.window.gBrowser.selectedBrowser;
|
||||
if (!RecommendationMap.has(browser)) {
|
||||
// There's no recommendation for this browser, so the user shouldn't have
|
||||
// been able to click
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
const {content} = RecommendationMap.get(browser);
|
||||
|
||||
// The recommendation should remain either collapsed or expanded while the
|
||||
// doorhanger is showing
|
||||
this._clearScheduledStateChanges();
|
||||
|
||||
// A hacky way of setting the popup anchor outside the usual url bar icon box
|
||||
// See https://searchfox.org/mozilla-central/rev/847b64cc28b74b44c379f9bff4f415b97da1c6d7/toolkit/modules/PopupNotifications.jsm#42
|
||||
browser.cfrpopupnotificationanchor = this.container;
|
||||
|
||||
const {primary, secondary} = content.buttons;
|
||||
|
||||
const mainAction = {
|
||||
label: primary.label,
|
||||
accessKey: primary.accessKey,
|
||||
callback: () => this._dispatchToASRouter(primary.action)
|
||||
};
|
||||
|
||||
const secondaryActions = [{
|
||||
label: secondary.label,
|
||||
accessKey: secondary.accessKey,
|
||||
callback: this._collapse
|
||||
}];
|
||||
|
||||
const options = {
|
||||
popupIconURL: content.addon.icon,
|
||||
hideClose: true,
|
||||
eventCallback: this._popupStateChange
|
||||
};
|
||||
|
||||
this.window.PopupNotifications.show(
|
||||
browser,
|
||||
POPUP_NOTIFICATION_ID,
|
||||
content.text,
|
||||
"cfr",
|
||||
mainAction,
|
||||
secondaryActions,
|
||||
options
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function isHostMatch(browser, host) {
|
||||
return (browser.documentURI.scheme.startsWith("http") &&
|
||||
browser.documentURI.host === host);
|
||||
}
|
||||
|
||||
const CFRPageActions = {
|
||||
// For testing purposes
|
||||
RecommendationMap,
|
||||
PageActionMap,
|
||||
|
||||
/**
|
||||
* To be called from browser.js on a location change, passing in the browser
|
||||
* that's been updated
|
||||
*/
|
||||
updatePageActions(browser) {
|
||||
const win = browser.ownerGlobal;
|
||||
const pageAction = PageActionMap.get(win);
|
||||
if (!pageAction || browser !== win.gBrowser.selectedBrowser) {
|
||||
return;
|
||||
}
|
||||
if (RecommendationMap.has(browser)) {
|
||||
const {host, content} = RecommendationMap.get(browser);
|
||||
if (isHostMatch(browser, host)) {
|
||||
// The browser has a recommendation specified with this host, so show
|
||||
// the page action
|
||||
pageAction.show(content.notification_text);
|
||||
} else {
|
||||
// The user has navigated away from the specified host in the given
|
||||
// browser, so the recommendation is no longer valid and should be removed
|
||||
RecommendationMap.delete(browser);
|
||||
pageAction.hide();
|
||||
}
|
||||
} else {
|
||||
// There's no recommendation specified for this browser, so hide the page action
|
||||
pageAction.hide();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a recommendation specific to the given browser and host.
|
||||
* @param browser The browser for the recommendation
|
||||
* @param host The host for the recommendation
|
||||
* @param recommendation The recommendation to show
|
||||
* @param dispatchToASRouter A function to dispatch resulting actions to
|
||||
* @param force Force the recommendation to appear if the host doesn't match
|
||||
* @return Did adding the recommendation succeed?
|
||||
*/
|
||||
async addRecommendation(browser, host, recommendation, dispatchToASRouter, force = false) {
|
||||
const win = browser.ownerGlobal;
|
||||
if (browser !== win.gBrowser.selectedBrowser || !(force || isHostMatch(browser, host))) {
|
||||
return false;
|
||||
}
|
||||
const {id, content} = recommendation;
|
||||
RecommendationMap.set(browser, {id, host, content});
|
||||
if (!PageActionMap.has(win)) {
|
||||
PageActionMap.set(win, new PageAction(win, dispatchToASRouter));
|
||||
}
|
||||
await PageActionMap.get(win).show(recommendation.content.notification_text, true);
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear all recommendations and hide all PageActions
|
||||
*/
|
||||
clearRecommendations() {
|
||||
for (const [win, pageAction] of PageActionMap) {
|
||||
pageAction.hide();
|
||||
PageActionMap.delete(win);
|
||||
}
|
||||
RecommendationMap.clear();
|
||||
}
|
||||
};
|
||||
|
||||
const EXPORTED_SYMBOLS = ["CFRPageActions"];
|
Загрузка…
Ссылка в новой задаче