forked from mirrors/gecko-dev
Differential Revision: https://phabricator.services.mozilla.com/D10976 --HG-- extra : moz-landing-system : lando
10424 lines
No EOL
378 KiB
JavaScript
10424 lines
No EOL
378 KiB
JavaScript
/******/ (function(modules) { // webpackBootstrap
|
|
/******/ // The module cache
|
|
/******/ var installedModules = {};
|
|
/******/
|
|
/******/ // The require function
|
|
/******/ function __webpack_require__(moduleId) {
|
|
/******/
|
|
/******/ // Check if module is in cache
|
|
/******/ if(installedModules[moduleId]) {
|
|
/******/ return installedModules[moduleId].exports;
|
|
/******/ }
|
|
/******/ // Create a new module (and put it into the cache)
|
|
/******/ var module = installedModules[moduleId] = {
|
|
/******/ i: moduleId,
|
|
/******/ l: false,
|
|
/******/ exports: {}
|
|
/******/ };
|
|
/******/
|
|
/******/ // Execute the module function
|
|
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
|
/******/
|
|
/******/ // Flag the module as loaded
|
|
/******/ module.l = true;
|
|
/******/
|
|
/******/ // Return the exports of the module
|
|
/******/ return module.exports;
|
|
/******/ }
|
|
/******/
|
|
/******/
|
|
/******/ // expose the modules object (__webpack_modules__)
|
|
/******/ __webpack_require__.m = modules;
|
|
/******/
|
|
/******/ // expose the module cache
|
|
/******/ __webpack_require__.c = installedModules;
|
|
/******/
|
|
/******/ // define getter function for harmony exports
|
|
/******/ __webpack_require__.d = function(exports, name, getter) {
|
|
/******/ if(!__webpack_require__.o(exports, name)) {
|
|
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
|
|
/******/ }
|
|
/******/ };
|
|
/******/
|
|
/******/ // define __esModule on exports
|
|
/******/ __webpack_require__.r = function(exports) {
|
|
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
|
|
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
/******/ }
|
|
/******/ Object.defineProperty(exports, '__esModule', { value: true });
|
|
/******/ };
|
|
/******/
|
|
/******/ // create a fake namespace object
|
|
/******/ // mode & 1: value is a module id, require it
|
|
/******/ // mode & 2: merge all properties of value into the ns
|
|
/******/ // mode & 4: return value when already ns object
|
|
/******/ // mode & 8|1: behave like require
|
|
/******/ __webpack_require__.t = function(value, mode) {
|
|
/******/ if(mode & 1) value = __webpack_require__(value);
|
|
/******/ if(mode & 8) return value;
|
|
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
|
|
/******/ var ns = Object.create(null);
|
|
/******/ __webpack_require__.r(ns);
|
|
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
|
|
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
|
|
/******/ return ns;
|
|
/******/ };
|
|
/******/
|
|
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
|
/******/ __webpack_require__.n = function(module) {
|
|
/******/ var getter = module && module.__esModule ?
|
|
/******/ function getDefault() { return module['default']; } :
|
|
/******/ function getModuleExports() { return module; };
|
|
/******/ __webpack_require__.d(getter, 'a', getter);
|
|
/******/ return getter;
|
|
/******/ };
|
|
/******/
|
|
/******/ // Object.prototype.hasOwnProperty.call
|
|
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
|
|
/******/
|
|
/******/ // __webpack_public_path__
|
|
/******/ __webpack_require__.p = "";
|
|
/******/
|
|
/******/
|
|
/******/ // Load entry module and return exports
|
|
/******/ return __webpack_require__(__webpack_require__.s = 0);
|
|
/******/ })
|
|
/************************************************************************/
|
|
/******/ ([
|
|
/* 0 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* WEBPACK VAR INJECTION */(function(global) {/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
|
|
/* harmony import */ var content_src_lib_snippets__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3);
|
|
/* harmony import */ var content_src_asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4);
|
|
/* harmony import */ var content_src_components_Base_Base__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(12);
|
|
/* harmony import */ var content_src_lib_detect_user_session_start__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(40);
|
|
/* harmony import */ var content_src_lib_asroutercontent__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(41);
|
|
/* harmony import */ var content_src_lib_init_store__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(5);
|
|
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(16);
|
|
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_7__);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(9);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_8___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_8__);
|
|
/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(11);
|
|
/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_9___default = /*#__PURE__*/__webpack_require__.n(react_dom__WEBPACK_IMPORTED_MODULE_9__);
|
|
/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(45);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const store = Object(content_src_lib_init_store__WEBPACK_IMPORTED_MODULE_6__["initStore"])(common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_10__["reducers"], global.gActivityStreamPrerenderedState);
|
|
const asrouterContent = new content_src_asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_2__["ASRouterContent"]();
|
|
|
|
new content_src_lib_detect_user_session_start__WEBPACK_IMPORTED_MODULE_4__["DetectUserSessionStart"](store).sendEventOrAddListener();
|
|
|
|
// If we are starting in a prerendered state, we must wait until the first render
|
|
// to request state rehydration (see Base.jsx). If we are NOT in a prerendered state,
|
|
// we can request it immedately.
|
|
if (!global.gActivityStreamPrerenderedState) {
|
|
store.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({ type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].NEW_TAB_STATE_REQUEST }));
|
|
}
|
|
|
|
react_dom__WEBPACK_IMPORTED_MODULE_9___default.a.hydrate(react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(
|
|
react_redux__WEBPACK_IMPORTED_MODULE_7__["Provider"],
|
|
{ store: store },
|
|
react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_Base_Base__WEBPACK_IMPORTED_MODULE_3__["Base"], {
|
|
isFirstrun: global.document.location.href === "about:welcome",
|
|
isPrerendered: !!global.gActivityStreamPrerenderedState,
|
|
locale: global.document.documentElement.lang,
|
|
strings: global.gActivityStreamStrings })
|
|
), document.getElementById("root"));
|
|
|
|
Object(content_src_lib_asroutercontent__WEBPACK_IMPORTED_MODULE_5__["enableASRouterContent"])(store, asrouterContent);
|
|
Object(content_src_lib_snippets__WEBPACK_IMPORTED_MODULE_1__["addSnippetsSubscriber"])(store);
|
|
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
|
|
|
|
/***/ }),
|
|
/* 1 */
|
|
/***/ (function(module, exports) {
|
|
|
|
var g;
|
|
|
|
// This works in non-strict mode
|
|
g = (function() {
|
|
return this;
|
|
})();
|
|
|
|
try {
|
|
// This works if eval is allowed (see CSP)
|
|
g = g || Function("return this")() || (1, eval)("this");
|
|
} catch (e) {
|
|
// This works if the window reference is available
|
|
if (typeof window === "object") g = window;
|
|
}
|
|
|
|
// g can still be undefined, but nothing to do about it...
|
|
// We return undefined, instead of nothing here, so it's
|
|
// easier to handle this case. if(!global) { ...}
|
|
|
|
module.exports = g;
|
|
|
|
|
|
/***/ }),
|
|
/* 2 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MAIN_MESSAGE_TYPE", function() { return MAIN_MESSAGE_TYPE; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "CONTENT_MESSAGE_TYPE", function() { return CONTENT_MESSAGE_TYPE; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "PRELOAD_MESSAGE_TYPE", function() { return PRELOAD_MESSAGE_TYPE; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "UI_CODE", function() { return UI_CODE; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "BACKGROUND_PROCESS", function() { return BACKGROUND_PROCESS; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "globalImportContext", function() { return globalImportContext; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "actionTypes", function() { return actionTypes; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ASRouterActions", function() { return ASRouterActions; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "actionCreators", function() { return actionCreators; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "actionUtils", function() { return actionUtils; });
|
|
/* 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/. */
|
|
|
|
|
|
var MAIN_MESSAGE_TYPE = "ActivityStream:Main";
|
|
var CONTENT_MESSAGE_TYPE = "ActivityStream:Content";
|
|
var PRELOAD_MESSAGE_TYPE = "ActivityStream:PreloadedBrowser";
|
|
var UI_CODE = 1;
|
|
var BACKGROUND_PROCESS = 2;
|
|
|
|
/**
|
|
* globalImportContext - Are we in UI code (i.e. react, a dom) or some kind of background process?
|
|
* Use this in action creators if you need different logic
|
|
* for ui/background processes.
|
|
*/
|
|
|
|
const globalImportContext = typeof Window === "undefined" ? BACKGROUND_PROCESS : UI_CODE;
|
|
// Export for tests
|
|
|
|
// Create an object that avoids accidental differing key/value pairs:
|
|
// {
|
|
// INIT: "INIT",
|
|
// UNINIT: "UNINIT"
|
|
// }
|
|
const actionTypes = {};
|
|
|
|
for (const type of ["ADDONS_INFO_REQUEST", "ADDONS_INFO_RESPONSE", "ARCHIVE_FROM_POCKET", "AS_ROUTER_INITIALIZED", "AS_ROUTER_PREF_CHANGED", "AS_ROUTER_TELEMETRY_USER_EVENT", "BLOCK_URL", "BOOKMARK_URL", "COPY_DOWNLOAD_LINK", "DELETE_BOOKMARK_BY_ID", "DELETE_FROM_POCKET", "DELETE_HISTORY_URL", "DIALOG_CANCEL", "DIALOG_OPEN", "DOWNLOAD_CHANGED", "FILL_SEARCH_TERM", "INIT", "MIGRATION_CANCEL", "MIGRATION_COMPLETED", "MIGRATION_START", "NEW_TAB_INIT", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_REHYDRATED", "NEW_TAB_STATE_REQUEST", "NEW_TAB_UNLOAD", "OPEN_DOWNLOAD_FILE", "OPEN_LINK", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "OPEN_WEBEXT_SETTINGS", "PAGE_PRERENDERED", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINKS_CHANGED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "PLACES_SAVED_TO_POCKET", "POCKET_CTA", "POCKET_LOGGED_IN", "POCKET_WAITING_FOR_SPOC", "PREFS_INITIAL_VALUES", "PREF_CHANGED", "PREVIEW_REQUEST", "PREVIEW_REQUEST_CANCEL", "PREVIEW_RESPONSE", "REMOVE_DOWNLOAD_FILE", "RICH_ICON_MISSING", "SAVE_SESSION_PERF_DATA", "SAVE_TO_POCKET", "SCREENSHOT_UPDATED", "SECTION_DEREGISTER", "SECTION_DISABLE", "SECTION_ENABLE", "SECTION_MOVE", "SECTION_OPTIONS_CHANGED", "SECTION_REGISTER", "SECTION_UPDATE", "SECTION_UPDATE_CARD", "SETTINGS_CLOSE", "SETTINGS_OPEN", "SET_PREF", "SHOW_DOWNLOAD_FILE", "SHOW_FIREFOX_ACCOUNTS", "SKIPPED_SIGNIN", "SNIPPETS_BLOCKLIST_CLEARED", "SNIPPETS_BLOCKLIST_UPDATED", "SNIPPETS_DATA", "SNIPPETS_PREVIEW_MODE", "SNIPPETS_RESET", "SNIPPET_BLOCKED", "SUBMIT_EMAIL", "SYSTEM_TICK", "TELEMETRY_IMPRESSION_STATS", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_CANCEL_EDIT", "TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL", "TOP_SITES_EDIT", "TOP_SITES_INSERT", "TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL", "TOP_SITES_PIN", "TOP_SITES_PREFS_UPDATED", "TOP_SITES_UNPIN", "TOP_SITES_UPDATED", "TOTAL_BOOKMARKS_REQUEST", "TOTAL_BOOKMARKS_RESPONSE", "UNINIT", "UPDATE_PINNED_SEARCH_SHORTCUTS", "UPDATE_SEARCH_SHORTCUTS", "UPDATE_SECTION_PREFS", "WEBEXT_CLICK", "WEBEXT_DISMISS"]) {
|
|
actionTypes[type] = type;
|
|
}
|
|
|
|
// These are acceptable actions for AS Router messages to have. They can show up
|
|
// as call-to-action buttons in snippets, onboarding tour, etc.
|
|
const ASRouterActions = {};
|
|
|
|
for (const type of ["INSTALL_ADDON_FROM_URL", "OPEN_APPLICATIONS_MENU", "OPEN_PRIVATE_BROWSER_WINDOW", "OPEN_URL", "OPEN_ABOUT_PAGE", "OPEN_PREFERENCES_PAGE", "SHOW_FIREFOX_ACCOUNTS"]) {
|
|
ASRouterActions[type] = type;
|
|
}
|
|
|
|
// Helper function for creating routed actions between content and main
|
|
// Not intended to be used by consumers
|
|
function _RouteMessage(action, options) {
|
|
const meta = action.meta ? Object.assign({}, action.meta) : {};
|
|
if (!options || !options.from || !options.to) {
|
|
throw new Error("Routed Messages must have options as the second parameter, and must at least include a .from and .to property.");
|
|
}
|
|
// For each of these fields, if they are passed as an option,
|
|
// add them to the action. If they are not defined, remove them.
|
|
["from", "to", "toTarget", "fromTarget", "skipMain", "skipLocal"].forEach(o => {
|
|
if (typeof options[o] !== "undefined") {
|
|
meta[o] = options[o];
|
|
} else if (meta[o]) {
|
|
delete meta[o];
|
|
}
|
|
});
|
|
return Object.assign({}, action, { meta });
|
|
}
|
|
|
|
/**
|
|
* AlsoToMain - Creates a message that will be dispatched locally and also sent to the Main process.
|
|
*
|
|
* @param {object} action Any redux action (required)
|
|
* @param {object} options
|
|
* @param {bool} skipLocal Used by OnlyToMain to skip the main reducer
|
|
* @param {string} fromTarget The id of the content port from which the action originated. (optional)
|
|
* @return {object} An action with added .meta properties
|
|
*/
|
|
function AlsoToMain(action, fromTarget, skipLocal) {
|
|
return _RouteMessage(action, {
|
|
from: CONTENT_MESSAGE_TYPE,
|
|
to: MAIN_MESSAGE_TYPE,
|
|
fromTarget,
|
|
skipLocal
|
|
});
|
|
}
|
|
|
|
/**
|
|
* OnlyToMain - Creates a message that will be sent to the Main process and skip the local reducer.
|
|
*
|
|
* @param {object} action Any redux action (required)
|
|
* @param {object} options
|
|
* @param {string} fromTarget The id of the content port from which the action originated. (optional)
|
|
* @return {object} An action with added .meta properties
|
|
*/
|
|
function OnlyToMain(action, fromTarget) {
|
|
return AlsoToMain(action, fromTarget, true);
|
|
}
|
|
|
|
/**
|
|
* BroadcastToContent - Creates a message that will be dispatched to main and sent to ALL content processes.
|
|
*
|
|
* @param {object} action Any redux action (required)
|
|
* @return {object} An action with added .meta properties
|
|
*/
|
|
function BroadcastToContent(action) {
|
|
return _RouteMessage(action, {
|
|
from: MAIN_MESSAGE_TYPE,
|
|
to: CONTENT_MESSAGE_TYPE
|
|
});
|
|
}
|
|
|
|
/**
|
|
* AlsoToOneContent - Creates a message that will be will be dispatched to the main store
|
|
* and also sent to a particular Content process.
|
|
*
|
|
* @param {object} action Any redux action (required)
|
|
* @param {string} target The id of a content port
|
|
* @param {bool} skipMain Used by OnlyToOneContent to skip the main process
|
|
* @return {object} An action with added .meta properties
|
|
*/
|
|
function AlsoToOneContent(action, target, skipMain) {
|
|
if (!target) {
|
|
throw new Error("You must provide a target ID as the second parameter of AlsoToOneContent. If you want to send to all content processes, use BroadcastToContent");
|
|
}
|
|
return _RouteMessage(action, {
|
|
from: MAIN_MESSAGE_TYPE,
|
|
to: CONTENT_MESSAGE_TYPE,
|
|
toTarget: target,
|
|
skipMain
|
|
});
|
|
}
|
|
|
|
/**
|
|
* OnlyToOneContent - Creates a message that will be sent to a particular Content process
|
|
* and skip the main reducer.
|
|
*
|
|
* @param {object} action Any redux action (required)
|
|
* @param {string} target The id of a content port
|
|
* @return {object} An action with added .meta properties
|
|
*/
|
|
function OnlyToOneContent(action, target) {
|
|
return AlsoToOneContent(action, target, true);
|
|
}
|
|
|
|
/**
|
|
* AlsoToPreloaded - Creates a message that dispatched to the main reducer and also sent to the preloaded tab.
|
|
*
|
|
* @param {object} action Any redux action (required)
|
|
* @return {object} An action with added .meta properties
|
|
*/
|
|
function AlsoToPreloaded(action) {
|
|
return _RouteMessage(action, {
|
|
from: MAIN_MESSAGE_TYPE,
|
|
to: PRELOAD_MESSAGE_TYPE
|
|
});
|
|
}
|
|
|
|
/**
|
|
* UserEvent - A telemetry ping indicating a user action. This should only
|
|
* be sent from the UI during a user session.
|
|
*
|
|
* @param {object} data Fields to include in the ping (source, etc.)
|
|
* @return {object} An AlsoToMain action
|
|
*/
|
|
function UserEvent(data) {
|
|
return AlsoToMain({
|
|
type: actionTypes.TELEMETRY_USER_EVENT,
|
|
data
|
|
});
|
|
}
|
|
|
|
/**
|
|
* ASRouterUserEvent - A telemetry ping indicating a user action from AS router. This should only
|
|
* be sent from the UI during a user session.
|
|
*
|
|
* @param {object} data Fields to include in the ping (source, etc.)
|
|
* @return {object} An AlsoToMain action
|
|
*/
|
|
function ASRouterUserEvent(data) {
|
|
return AlsoToMain({
|
|
type: actionTypes.AS_ROUTER_TELEMETRY_USER_EVENT,
|
|
data
|
|
});
|
|
}
|
|
|
|
/**
|
|
* UndesiredEvent - A telemetry ping indicating an undesired state.
|
|
*
|
|
* @param {object} data Fields to include in the ping (value, etc.)
|
|
* @param {int} importContext (For testing) Override the import context for testing.
|
|
* @return {object} An action. For UI code, a AlsoToMain action.
|
|
*/
|
|
function UndesiredEvent(data, importContext = globalImportContext) {
|
|
const action = {
|
|
type: actionTypes.TELEMETRY_UNDESIRED_EVENT,
|
|
data
|
|
};
|
|
return importContext === UI_CODE ? AlsoToMain(action) : action;
|
|
}
|
|
|
|
/**
|
|
* PerfEvent - A telemetry ping indicating a performance-related event.
|
|
*
|
|
* @param {object} data Fields to include in the ping (value, etc.)
|
|
* @param {int} importContext (For testing) Override the import context for testing.
|
|
* @return {object} An action. For UI code, a AlsoToMain action.
|
|
*/
|
|
function PerfEvent(data, importContext = globalImportContext) {
|
|
const action = {
|
|
type: actionTypes.TELEMETRY_PERFORMANCE_EVENT,
|
|
data
|
|
};
|
|
return importContext === UI_CODE ? AlsoToMain(action) : action;
|
|
}
|
|
|
|
/**
|
|
* ImpressionStats - A telemetry ping indicating an impression stats.
|
|
*
|
|
* @param {object} data Fields to include in the ping
|
|
* @param {int} importContext (For testing) Override the import context for testing.
|
|
* #return {object} An action. For UI code, a AlsoToMain action.
|
|
*/
|
|
function ImpressionStats(data, importContext = globalImportContext) {
|
|
const action = {
|
|
type: actionTypes.TELEMETRY_IMPRESSION_STATS,
|
|
data
|
|
};
|
|
return importContext === UI_CODE ? AlsoToMain(action) : action;
|
|
}
|
|
|
|
function SetPref(name, value, importContext = globalImportContext) {
|
|
const action = { type: actionTypes.SET_PREF, data: { name, value } };
|
|
return importContext === UI_CODE ? AlsoToMain(action) : action;
|
|
}
|
|
|
|
function WebExtEvent(type, data, importContext = globalImportContext) {
|
|
if (!data || !data.source) {
|
|
throw new Error("WebExtEvent actions should include a property \"source\", the id of the webextension that should receive the event.");
|
|
}
|
|
const action = { type, data };
|
|
return importContext === UI_CODE ? AlsoToMain(action) : action;
|
|
}
|
|
|
|
var actionCreators = {
|
|
BroadcastToContent,
|
|
UserEvent,
|
|
ASRouterUserEvent,
|
|
UndesiredEvent,
|
|
PerfEvent,
|
|
ImpressionStats,
|
|
AlsoToOneContent,
|
|
OnlyToOneContent,
|
|
AlsoToMain,
|
|
OnlyToMain,
|
|
AlsoToPreloaded,
|
|
SetPref,
|
|
WebExtEvent
|
|
};
|
|
|
|
// These are helpers to test for certain kinds of actions
|
|
|
|
var actionUtils = {
|
|
isSendToMain(action) {
|
|
if (!action.meta) {
|
|
return false;
|
|
}
|
|
return action.meta.to === MAIN_MESSAGE_TYPE && action.meta.from === CONTENT_MESSAGE_TYPE;
|
|
},
|
|
isBroadcastToContent(action) {
|
|
if (!action.meta) {
|
|
return false;
|
|
}
|
|
if (action.meta.to === CONTENT_MESSAGE_TYPE && !action.meta.toTarget) {
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
isSendToOneContent(action) {
|
|
if (!action.meta) {
|
|
return false;
|
|
}
|
|
if (action.meta.to === CONTENT_MESSAGE_TYPE && action.meta.toTarget) {
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
isSendToPreloaded(action) {
|
|
if (!action.meta) {
|
|
return false;
|
|
}
|
|
return action.meta.to === PRELOAD_MESSAGE_TYPE && action.meta.from === MAIN_MESSAGE_TYPE;
|
|
},
|
|
isFromMain(action) {
|
|
if (!action.meta) {
|
|
return false;
|
|
}
|
|
return action.meta.from === MAIN_MESSAGE_TYPE && action.meta.to === CONTENT_MESSAGE_TYPE;
|
|
},
|
|
getPortIdOfSender(action) {
|
|
return action.meta && action.meta.fromTarget || null;
|
|
},
|
|
_RouteMessage
|
|
};
|
|
|
|
/***/ }),
|
|
/* 3 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SNIPPETS_UPDATE_INTERVAL_MS", function() { return SNIPPETS_UPDATE_INTERVAL_MS; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SnippetsMap", function() { return SnippetsMap; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SnippetsProvider", function() { return SnippetsProvider; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "addSnippetsSubscriber", function() { return addSnippetsSubscriber; });
|
|
/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
|
|
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
|
|
|
|
const DATABASE_NAME = "snippets_db";
|
|
const DATABASE_VERSION = 1;
|
|
const SNIPPETS_OBJECTSTORE_NAME = "snippets";
|
|
const SNIPPETS_UPDATE_INTERVAL_MS = 14400000; // 4 hours.
|
|
|
|
const SNIPPETS_ENABLED_EVENT = "Snippets:Enabled";
|
|
const SNIPPETS_DISABLED_EVENT = "Snippets:Disabled";
|
|
|
|
|
|
|
|
/**
|
|
* SnippetsMap - A utility for cacheing values related to the snippet. It has
|
|
* the same interface as a Map, but is optionally backed by
|
|
* indexedDB for persistent storage.
|
|
* Call .connect() to open a database connection and restore any
|
|
* previously cached data, if necessary.
|
|
*
|
|
*/
|
|
class SnippetsMap extends Map {
|
|
constructor(dispatch) {
|
|
super();
|
|
this._db = null;
|
|
this._dispatch = dispatch;
|
|
}
|
|
|
|
set(key, value) {
|
|
super.set(key, value);
|
|
return this._dbTransaction(db => db.put(value, key));
|
|
}
|
|
|
|
delete(key) {
|
|
super.delete(key);
|
|
return this._dbTransaction(db => db.delete(key));
|
|
}
|
|
|
|
clear() {
|
|
super.clear();
|
|
this._dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({ type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].SNIPPETS_BLOCKLIST_CLEARED }));
|
|
return this._dbTransaction(db => db.clear());
|
|
}
|
|
|
|
get blockList() {
|
|
return this.get("blockList") || [];
|
|
}
|
|
|
|
/**
|
|
* blockSnippetById - Blocks a snippet given an id
|
|
*
|
|
* @param {str|int} id The id of the snippet
|
|
* @return {Promise} Resolves when the id has been written to indexedDB,
|
|
* or immediately if the snippetMap is not connected
|
|
*/
|
|
blockSnippetById(id) {
|
|
var _this = this;
|
|
|
|
return _asyncToGenerator(function* () {
|
|
if (!id) {
|
|
return;
|
|
}
|
|
const { blockList } = _this;
|
|
if (!blockList.includes(id)) {
|
|
blockList.push(id);
|
|
_this._dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({ type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].SNIPPETS_BLOCKLIST_UPDATED, data: id }));
|
|
yield _this.set("blockList", blockList);
|
|
}
|
|
})();
|
|
}
|
|
|
|
disableOnboarding() {}
|
|
|
|
showFirefoxAccounts() {
|
|
this._dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({ type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].SHOW_FIREFOX_ACCOUNTS }));
|
|
}
|
|
|
|
getTotalBookmarksCount() {
|
|
return new Promise(resolve => {
|
|
this._dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({ type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].TOTAL_BOOKMARKS_REQUEST }));
|
|
global.RPMAddMessageListener("ActivityStream:MainToContent", function onMessage({ data: action }) {
|
|
if (action.type === common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].TOTAL_BOOKMARKS_RESPONSE) {
|
|
resolve(action.data);
|
|
global.RPMRemoveMessageListener("ActivityStream:MainToContent", onMessage);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
getAddonsInfo() {
|
|
return new Promise(resolve => {
|
|
this._dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({ type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].ADDONS_INFO_REQUEST }));
|
|
global.RPMAddMessageListener("ActivityStream:MainToContent", function onMessage({ data: action }) {
|
|
if (action.type === common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].ADDONS_INFO_RESPONSE) {
|
|
resolve(action.data);
|
|
global.RPMRemoveMessageListener("ActivityStream:MainToContent", onMessage);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* connect - Attaches an indexedDB back-end to the Map so that any set values
|
|
* are also cached in a store. It also restores any existing values
|
|
* that are already stored in the indexedDB store.
|
|
*
|
|
* @return {type} description
|
|
*/
|
|
connect() {
|
|
var _this2 = this;
|
|
|
|
return _asyncToGenerator(function* () {
|
|
// Open the connection
|
|
const db = yield _this2._openDB();
|
|
|
|
// Restore any existing values
|
|
yield _this2._restoreFromDb(db);
|
|
|
|
// Attach a reference to the db
|
|
_this2._db = db;
|
|
})();
|
|
}
|
|
|
|
/**
|
|
* _dbTransaction - Returns a db transaction wrapped with the given modifier
|
|
* function as a Promise. If the db has not been connected,
|
|
* it resolves immediately.
|
|
*
|
|
* @param {func} modifier A function to call with the transaction
|
|
* @return {obj} A Promise that resolves when the transaction has
|
|
* completed or errored
|
|
*/
|
|
_dbTransaction(modifier) {
|
|
if (!this._db) {
|
|
return Promise.resolve();
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
const transaction = modifier(this._db.transaction(SNIPPETS_OBJECTSTORE_NAME, "readwrite").objectStore(SNIPPETS_OBJECTSTORE_NAME));
|
|
transaction.onsuccess = event => resolve();
|
|
|
|
/* istanbul ignore next */
|
|
transaction.onerror = event => reject(transaction.error);
|
|
});
|
|
}
|
|
|
|
_openDB() {
|
|
return new Promise((resolve, reject) => {
|
|
const openRequest = indexedDB.open(DATABASE_NAME, DATABASE_VERSION);
|
|
|
|
/* istanbul ignore next */
|
|
openRequest.onerror = event => {
|
|
// Try to delete the old database so that we can start this process over
|
|
// next time.
|
|
indexedDB.deleteDatabase(DATABASE_NAME);
|
|
reject(event);
|
|
};
|
|
|
|
openRequest.onupgradeneeded = event => {
|
|
const db = event.target.result;
|
|
if (!db.objectStoreNames.contains(SNIPPETS_OBJECTSTORE_NAME)) {
|
|
db.createObjectStore(SNIPPETS_OBJECTSTORE_NAME);
|
|
}
|
|
};
|
|
|
|
openRequest.onsuccess = event => {
|
|
let db = event.target.result;
|
|
|
|
/* istanbul ignore next */
|
|
db.onerror = err => console.error(err); // eslint-disable-line no-console
|
|
/* istanbul ignore next */
|
|
db.onversionchange = versionChangeEvent => versionChangeEvent.target.close();
|
|
|
|
resolve(db);
|
|
};
|
|
});
|
|
}
|
|
|
|
_restoreFromDb(db) {
|
|
return new Promise((resolve, reject) => {
|
|
let cursorRequest;
|
|
try {
|
|
cursorRequest = db.transaction(SNIPPETS_OBJECTSTORE_NAME).objectStore(SNIPPETS_OBJECTSTORE_NAME).openCursor();
|
|
} catch (err) {
|
|
// istanbul ignore next
|
|
reject(err);
|
|
// istanbul ignore next
|
|
return;
|
|
}
|
|
|
|
/* istanbul ignore next */
|
|
cursorRequest.onerror = event => reject(event);
|
|
|
|
cursorRequest.onsuccess = event => {
|
|
let cursor = event.target.result;
|
|
// Populate the cache from the persistent storage.
|
|
if (cursor) {
|
|
if (cursor.value !== "blockList") {
|
|
this.set(cursor.key, cursor.value);
|
|
}
|
|
cursor.continue();
|
|
} else {
|
|
// We are done.
|
|
resolve();
|
|
}
|
|
};
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* SnippetsProvider - Initializes a SnippetsMap and loads snippets from a
|
|
* remote location, or else default snippets if the remote
|
|
* snippets cannot be retrieved.
|
|
*/
|
|
class SnippetsProvider {
|
|
constructor(dispatch) {
|
|
// Initialize the Snippets Map and attaches it to a global so that
|
|
// the snippet payload can interact with it.
|
|
global.gSnippetsMap = new SnippetsMap(dispatch);
|
|
this._onAction = this._onAction.bind(this);
|
|
}
|
|
|
|
get snippetsMap() {
|
|
return global.gSnippetsMap;
|
|
}
|
|
|
|
_refreshSnippets() {
|
|
var _this3 = this;
|
|
|
|
return _asyncToGenerator(function* () {
|
|
// Check if the cached version of of the snippets in snippetsMap. If it's too
|
|
// old, blow away the entire snippetsMap.
|
|
const cachedVersion = _this3.snippetsMap.get("snippets-cached-version");
|
|
|
|
if (cachedVersion !== _this3.appData.version) {
|
|
_this3.snippetsMap.clear();
|
|
}
|
|
|
|
// Has enough time passed for us to require an update?
|
|
const lastUpdate = _this3.snippetsMap.get("snippets-last-update");
|
|
const needsUpdate = !(lastUpdate >= 0) || Date.now() - lastUpdate > SNIPPETS_UPDATE_INTERVAL_MS;
|
|
|
|
if (needsUpdate && _this3.appData.snippetsURL) {
|
|
_this3.snippetsMap.set("snippets-last-update", Date.now());
|
|
try {
|
|
const response = yield fetch(_this3.appData.snippetsURL);
|
|
if (response.status === 200) {
|
|
const payload = yield response.text();
|
|
|
|
_this3.snippetsMap.set("snippets", payload);
|
|
_this3.snippetsMap.set("snippets-cached-version", _this3.appData.version);
|
|
}
|
|
} catch (e) {
|
|
console.error(e); // eslint-disable-line no-console
|
|
}
|
|
}
|
|
})();
|
|
}
|
|
|
|
_noSnippetFallback() {
|
|
// TODO
|
|
}
|
|
|
|
_showRemoteSnippets() {
|
|
const snippetsEl = document.getElementById(this.elementId);
|
|
const payload = this.snippetsMap.get("snippets");
|
|
|
|
if (!snippetsEl) {
|
|
throw new Error(`No element was found with id '${this.elementId}'.`);
|
|
}
|
|
|
|
// This could happen if fetching failed
|
|
if (!payload) {
|
|
throw new Error("No remote snippets were found in gSnippetsMap.");
|
|
}
|
|
|
|
if (typeof payload !== "string") {
|
|
throw new Error("Snippet payload was incorrectly formatted");
|
|
}
|
|
|
|
// Note that injecting snippets can throw if they're invalid XML.
|
|
// eslint-disable-next-line no-unsanitized/property
|
|
snippetsEl.innerHTML = payload;
|
|
|
|
// Scripts injected by innerHTML are inactive, so we have to relocate them
|
|
// through DOM manipulation to activate their contents.
|
|
for (const scriptEl of snippetsEl.getElementsByTagName("script")) {
|
|
const relocatedScript = document.createElement("script");
|
|
relocatedScript.text = scriptEl.text;
|
|
scriptEl.parentNode.replaceChild(relocatedScript, scriptEl);
|
|
}
|
|
}
|
|
|
|
_onAction(msg) {
|
|
if (msg.data.type === common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].SNIPPET_BLOCKED) {
|
|
if (!this.snippetsMap.blockList.includes(msg.data.data)) {
|
|
this.snippetsMap.set("blockList", this.snippetsMap.blockList.concat(msg.data.data));
|
|
document.getElementById("snippets-container").style.display = "none";
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* init - Fetch the snippet payload and show snippets
|
|
*
|
|
* @param {obj} options
|
|
* @param {str} options.appData.snippetsURL The URL from which we fetch snippets
|
|
* @param {int} options.appData.version The current snippets version
|
|
* @param {str} options.elementId The id of the element in which to inject snippets
|
|
* @param {bool} options.connect Should gSnippetsMap connect to indexedDB?
|
|
*/
|
|
init(options) {
|
|
var _this4 = this;
|
|
|
|
return _asyncToGenerator(function* () {
|
|
Object.assign(_this4, {
|
|
appData: {},
|
|
elementId: "snippets",
|
|
connect: true
|
|
}, options);
|
|
|
|
// Add listener so we know when snippets are blocked on other pages
|
|
if (global.RPMAddMessageListener) {
|
|
global.RPMAddMessageListener("ActivityStream:MainToContent", _this4._onAction);
|
|
}
|
|
|
|
// TODO: Requires enabling indexedDB on newtab
|
|
// Restore the snippets map from indexedDB
|
|
if (_this4.connect) {
|
|
try {
|
|
yield _this4.snippetsMap.connect();
|
|
} catch (e) {
|
|
console.error(e); // eslint-disable-line no-console
|
|
}
|
|
}
|
|
|
|
// Cache app data values so they can be accessible from gSnippetsMap
|
|
for (const key of Object.keys(_this4.appData)) {
|
|
if (key === "blockList") {
|
|
_this4.snippetsMap.set("blockList", _this4.appData[key]);
|
|
} else {
|
|
_this4.snippetsMap.set(`appData.${key}`, _this4.appData[key]);
|
|
}
|
|
}
|
|
|
|
// Refresh snippets, if enough time has passed.
|
|
yield _this4._refreshSnippets();
|
|
|
|
// Try showing remote snippets, falling back to defaults if necessary.
|
|
try {
|
|
_this4._showRemoteSnippets();
|
|
} catch (e) {
|
|
_this4._noSnippetFallback(e);
|
|
}
|
|
|
|
window.dispatchEvent(new Event(SNIPPETS_ENABLED_EVENT));
|
|
|
|
_this4.initialized = true;
|
|
})();
|
|
}
|
|
|
|
uninit() {
|
|
window.dispatchEvent(new Event(SNIPPETS_DISABLED_EVENT));
|
|
if (global.RPMRemoveMessageListener) {
|
|
global.RPMRemoveMessageListener("ActivityStream:MainToContent", this._onAction);
|
|
}
|
|
this.initialized = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* addSnippetsSubscriber - Creates a SnippetsProvider that Initializes
|
|
* when the store has received the appropriate
|
|
* Snippet data.
|
|
*
|
|
* @param {obj} store The redux store
|
|
* @return {obj} Returns the snippets instance, asrouterContent instance and unsubscribe function
|
|
*/
|
|
function addSnippetsSubscriber(store) {
|
|
const snippets = new SnippetsProvider(store.dispatch);
|
|
|
|
let initializing = false;
|
|
|
|
store.subscribe(_asyncToGenerator(function* () {
|
|
const state = store.getState();
|
|
|
|
/**
|
|
* Sorry this code is so complicated. It will be removed soon.
|
|
* This is what the different values actually mean:
|
|
*
|
|
* ASRouter.initialized Is ASRouter.jsm initialised?
|
|
* ASRouter.allowLegacySnippets Are ASRouter snippets turned OFF (i.e. legacy snippets are allowed)
|
|
* state.Prefs.values["feeds.snippets"] User preference for snippets
|
|
* state.Snippets.initialized Is SnippetsFeed.jsm initialised?
|
|
* snippets.initialized Is in-content snippets currently initialised?
|
|
* state.Prefs.values.disableSnippets This pref is used to disable legacy snippets in an emergency
|
|
* in a way that is not user-editable (true = disabled)
|
|
*/
|
|
|
|
/** If we should initialize snippets... */
|
|
if (state.Prefs.values["feeds.snippets"] && state.ASRouter.initialized && state.ASRouter.allowLegacySnippets && !state.Prefs.values.disableSnippets && state.Snippets.initialized && !snippets.initialized &&
|
|
// Don't call init multiple times
|
|
!initializing && location.href !== "about:welcome" && location.hash !== "#asrouter") {
|
|
initializing = true;
|
|
yield snippets.init({ appData: state.Snippets });
|
|
// istanbul ignore if
|
|
if (state.Prefs.values["asrouter.devtoolsEnabled"]) {
|
|
console.log("Legacy snippets initialized"); // eslint-disable-line no-console
|
|
}
|
|
initializing = false;
|
|
|
|
/** If we should remove snippets... */
|
|
} else if ((state.Prefs.values["feeds.snippets"] === false || state.Prefs.values.disableSnippets === true || state.ASRouter.initialized && !state.ASRouter.allowLegacySnippets) && snippets.initialized) {
|
|
// Remove snippets
|
|
snippets.uninit();
|
|
// istanbul ignore if
|
|
if (state.Prefs.values["asrouter.devtoolsEnabled"]) {
|
|
console.log("Legacy snippets removed"); // eslint-disable-line no-console
|
|
}
|
|
}
|
|
}));
|
|
|
|
// Returned for testing purposes
|
|
return { snippets };
|
|
}
|
|
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
|
|
|
|
/***/ }),
|
|
/* 4 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ASRouterUtils", function() { return ASRouterUtils; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ASRouterUISurface", function() { return ASRouterUISurface; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ASRouterContent", function() { return ASRouterContent; });
|
|
/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
|
|
/* harmony import */ var content_src_lib_init_store__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(5);
|
|
/* harmony import */ var _rich_text_strings__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(7);
|
|
/* harmony import */ var _components_ImpressionsWrapper_ImpressionsWrapper__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(8);
|
|
/* harmony import */ var fluent_react__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(44);
|
|
/* harmony import */ var _templates_OnboardingMessage_OnboardingMessage__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(46);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(9);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_6__);
|
|
/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(11);
|
|
/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(react_dom__WEBPACK_IMPORTED_MODULE_7__);
|
|
/* harmony import */ var _templates_template_manifest__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(42);
|
|
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const INCOMING_MESSAGE_NAME = "ASRouter:parent-to-child";
|
|
const OUTGOING_MESSAGE_NAME = "ASRouter:child-to-parent";
|
|
const ASR_CONTAINER_ID = "asr-newtab-container";
|
|
|
|
const ASRouterUtils = {
|
|
addListener(listener) {
|
|
global.RPMAddMessageListener(INCOMING_MESSAGE_NAME, listener);
|
|
},
|
|
removeListener(listener) {
|
|
global.RPMRemoveMessageListener(INCOMING_MESSAGE_NAME, listener);
|
|
},
|
|
sendMessage(action) {
|
|
global.RPMSendAsyncMessage(OUTGOING_MESSAGE_NAME, action);
|
|
},
|
|
blockById(id, options) {
|
|
ASRouterUtils.sendMessage({ type: "BLOCK_MESSAGE_BY_ID", data: Object.assign({ id }, options) });
|
|
},
|
|
dismissById(id) {
|
|
ASRouterUtils.sendMessage({ type: "DISMISS_MESSAGE_BY_ID", data: { id } });
|
|
},
|
|
blockBundle(bundle) {
|
|
ASRouterUtils.sendMessage({ type: "BLOCK_BUNDLE", data: { bundle } });
|
|
},
|
|
executeAction(button_action) {
|
|
ASRouterUtils.sendMessage({
|
|
type: "USER_ACTION",
|
|
data: button_action
|
|
});
|
|
},
|
|
unblockById(id) {
|
|
ASRouterUtils.sendMessage({ type: "UNBLOCK_MESSAGE_BY_ID", data: { id } });
|
|
},
|
|
unblockBundle(bundle) {
|
|
ASRouterUtils.sendMessage({ type: "UNBLOCK_BUNDLE", data: { bundle } });
|
|
},
|
|
overrideMessage(id) {
|
|
ASRouterUtils.sendMessage({ type: "OVERRIDE_MESSAGE", data: { id } });
|
|
},
|
|
sendTelemetry(ping) {
|
|
const payload = common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].ASRouterUserEvent(ping);
|
|
global.RPMSendAsyncMessage(content_src_lib_init_store__WEBPACK_IMPORTED_MODULE_1__["OUTGOING_MESSAGE_NAME"], payload);
|
|
},
|
|
getPreviewEndpoint() {
|
|
if (window.location.href.includes("endpoint")) {
|
|
const params = new URLSearchParams(window.location.href.slice(window.location.href.indexOf("endpoint")));
|
|
try {
|
|
const endpoint = new URL(params.get("endpoint"));
|
|
return {
|
|
url: endpoint.href,
|
|
snippetId: params.get("snippetId")
|
|
};
|
|
} catch (e) {}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// Note: nextProps/prevProps refer to props passed to <ImpressionsWrapper />, not <ASRouterUISurface />
|
|
function shouldSendImpressionOnUpdate(nextProps, prevProps) {
|
|
return nextProps.message.id && (!prevProps.message || prevProps.message.id !== nextProps.message.id);
|
|
}
|
|
|
|
class ASRouterUISurface extends react__WEBPACK_IMPORTED_MODULE_6___default.a.PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
this.onMessageFromParent = this.onMessageFromParent.bind(this);
|
|
this.sendClick = this.sendClick.bind(this);
|
|
this.sendImpression = this.sendImpression.bind(this);
|
|
this.sendUserActionTelemetry = this.sendUserActionTelemetry.bind(this);
|
|
this.state = { message: {}, bundle: {} };
|
|
}
|
|
|
|
sendUserActionTelemetry(extraProps = {}) {
|
|
const { message, bundle } = this.state;
|
|
if (!message && !extraProps.message_id) {
|
|
throw new Error(`You must provide a message_id for bundled messages`);
|
|
}
|
|
const eventType = `${message.provider || bundle.provider}_user_event`;
|
|
ASRouterUtils.sendTelemetry(Object.assign({
|
|
message_id: message.id || extraProps.message_id,
|
|
source: extraProps.id,
|
|
action: eventType
|
|
}, extraProps));
|
|
}
|
|
|
|
sendImpression(extraProps) {
|
|
if (this.state.message.provider === "preview") {
|
|
return;
|
|
}
|
|
|
|
ASRouterUtils.sendMessage({ type: "IMPRESSION", data: this.state.message });
|
|
this.sendUserActionTelemetry(Object.assign({ event: "IMPRESSION" }, extraProps));
|
|
}
|
|
|
|
// If link has a `metric` data attribute send it as part of the `value`
|
|
// telemetry field which can have arbitrary values.
|
|
// Used for router messages with links as part of the content.
|
|
sendClick(event) {
|
|
const metric = {
|
|
value: event.target.dataset.metric,
|
|
// Used for the `source` of the event. Needed to differentiate
|
|
// from other snippet or onboarding events that may occur.
|
|
id: "NEWTAB_FOOTER_BAR_CONTENT"
|
|
};
|
|
const action = {
|
|
type: event.target.dataset.action,
|
|
data: { args: event.target.dataset.args }
|
|
};
|
|
if (action.type) {
|
|
ASRouterUtils.executeAction(action);
|
|
}
|
|
if (!this.state.message.content.do_not_autoblock && !event.target.dataset.do_not_autoblock) {
|
|
ASRouterUtils.blockById(this.state.message.id);
|
|
}
|
|
if (this.state.message.provider !== "preview") {
|
|
this.sendUserActionTelemetry(Object.assign({ event: "CLICK_BUTTON" }, metric));
|
|
}
|
|
}
|
|
|
|
onBlockById(id) {
|
|
return options => ASRouterUtils.blockById(id, options);
|
|
}
|
|
|
|
onDismissById(id) {
|
|
return () => ASRouterUtils.dismissById(id);
|
|
}
|
|
|
|
clearBundle(bundle) {
|
|
return () => ASRouterUtils.blockBundle(bundle);
|
|
}
|
|
|
|
onMessageFromParent({ data: action }) {
|
|
switch (action.type) {
|
|
case "SET_MESSAGE":
|
|
this.setState({ message: action.data });
|
|
break;
|
|
case "SET_BUNDLED_MESSAGES":
|
|
this.setState({ bundle: action.data });
|
|
break;
|
|
case "CLEAR_MESSAGE":
|
|
if (action.data.id === this.state.message.id) {
|
|
this.setState({ message: {} });
|
|
}
|
|
break;
|
|
case "CLEAR_PROVIDER":
|
|
if (action.data.id === this.state.message.provider) {
|
|
this.setState({ message: {} });
|
|
}
|
|
break;
|
|
case "CLEAR_BUNDLE":
|
|
if (this.state.bundle.bundle) {
|
|
this.setState({ bundle: {} });
|
|
}
|
|
break;
|
|
case "CLEAR_ALL":
|
|
this.setState({ message: {}, bundle: {} });
|
|
}
|
|
}
|
|
|
|
componentWillMount() {
|
|
const endpoint = ASRouterUtils.getPreviewEndpoint();
|
|
ASRouterUtils.addListener(this.onMessageFromParent);
|
|
|
|
// If we are loading about:welcome we want to trigger the onboarding messages
|
|
if (this.props.document.location.href === "about:welcome") {
|
|
ASRouterUtils.sendMessage({ type: "TRIGGER", data: { trigger: { id: "firstRun" } } });
|
|
} else {
|
|
ASRouterUtils.sendMessage({ type: "SNIPPETS_REQUEST", data: { endpoint } });
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
ASRouterUtils.removeListener(this.onMessageFromParent);
|
|
}
|
|
|
|
renderSnippets() {
|
|
const SnippetComponent = _templates_template_manifest__WEBPACK_IMPORTED_MODULE_8__["SnippetsTemplates"][this.state.message.template];
|
|
const { content } = this.state.message;
|
|
|
|
return react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(
|
|
_components_ImpressionsWrapper_ImpressionsWrapper__WEBPACK_IMPORTED_MODULE_3__["ImpressionsWrapper"],
|
|
{
|
|
id: "NEWTAB_FOOTER_BAR",
|
|
message: this.state.message,
|
|
sendImpression: this.sendImpression,
|
|
shouldSendImpressionOnUpdate: shouldSendImpressionOnUpdate
|
|
// This helps with testing
|
|
, document: this.props.document },
|
|
react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(
|
|
fluent_react__WEBPACK_IMPORTED_MODULE_4__["LocalizationProvider"],
|
|
{ messages: Object(_rich_text_strings__WEBPACK_IMPORTED_MODULE_2__["generateMessages"])(content) },
|
|
react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(SnippetComponent, _extends({}, this.state.message, {
|
|
UISurface: "NEWTAB_FOOTER_BAR",
|
|
onBlock: this.onBlockById(this.state.message.id),
|
|
onDismiss: this.onDismissById(this.state.message.id),
|
|
onAction: ASRouterUtils.executeAction,
|
|
sendClick: this.sendClick,
|
|
sendUserActionTelemetry: this.sendUserActionTelemetry }))
|
|
)
|
|
);
|
|
}
|
|
|
|
renderOnboarding() {
|
|
return react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(_templates_OnboardingMessage_OnboardingMessage__WEBPACK_IMPORTED_MODULE_5__["OnboardingMessage"], _extends({}, this.state.bundle, {
|
|
UISurface: "NEWTAB_OVERLAY",
|
|
onAction: ASRouterUtils.executeAction,
|
|
onDoneButton: this.clearBundle(this.state.bundle.bundle),
|
|
sendUserActionTelemetry: this.sendUserActionTelemetry }));
|
|
}
|
|
|
|
renderPreviewBanner() {
|
|
if (this.state.message.provider !== "preview") {
|
|
return null;
|
|
}
|
|
|
|
return react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(
|
|
"div",
|
|
{ className: "snippets-preview-banner" },
|
|
react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement("span", { className: "icon icon-small-spacer icon-info" }),
|
|
react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(
|
|
"span",
|
|
null,
|
|
"Preview Purposes Only"
|
|
)
|
|
);
|
|
}
|
|
|
|
render() {
|
|
const { message, bundle } = this.state;
|
|
if (!message.id && !bundle.template) {
|
|
return null;
|
|
}
|
|
return react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(
|
|
react__WEBPACK_IMPORTED_MODULE_6___default.a.Fragment,
|
|
null,
|
|
this.renderPreviewBanner(),
|
|
bundle.template === "onboarding" ? this.renderOnboarding() : this.renderSnippets()
|
|
);
|
|
}
|
|
}
|
|
|
|
ASRouterUISurface.defaultProps = { document: global.document };
|
|
|
|
class ASRouterContent {
|
|
constructor() {
|
|
this.initialized = false;
|
|
this.containerElement = null;
|
|
}
|
|
|
|
_mount() {
|
|
this.containerElement = global.document.getElementById(ASR_CONTAINER_ID);
|
|
if (!this.containerElement) {
|
|
this.containerElement = global.document.createElement("div");
|
|
this.containerElement.id = ASR_CONTAINER_ID;
|
|
this.containerElement.style.zIndex = 1;
|
|
global.document.body.appendChild(this.containerElement);
|
|
}
|
|
|
|
react_dom__WEBPACK_IMPORTED_MODULE_7___default.a.render(react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(ASRouterUISurface, null), this.containerElement);
|
|
}
|
|
|
|
_unmount() {
|
|
react_dom__WEBPACK_IMPORTED_MODULE_7___default.a.unmountComponentAtNode(this.containerElement);
|
|
}
|
|
|
|
init() {
|
|
this._mount();
|
|
this.initialized = true;
|
|
}
|
|
|
|
uninit() {
|
|
if (this.initialized) {
|
|
this._unmount();
|
|
this.initialized = false;
|
|
}
|
|
}
|
|
}
|
|
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
|
|
|
|
/***/ }),
|
|
/* 5 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MERGE_STORE_ACTION", function() { return MERGE_STORE_ACTION; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "OUTGOING_MESSAGE_NAME", function() { return OUTGOING_MESSAGE_NAME; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "INCOMING_MESSAGE_NAME", function() { return INCOMING_MESSAGE_NAME; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "EARLY_QUEUED_ACTIONS", function() { return EARLY_QUEUED_ACTIONS; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "rehydrationMiddleware", function() { return rehydrationMiddleware; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "queueEarlyMessageMiddleware", function() { return queueEarlyMessageMiddleware; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "initStore", function() { return initStore; });
|
|
/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
|
|
/* harmony import */ var redux__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6);
|
|
/* harmony import */ var redux__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(redux__WEBPACK_IMPORTED_MODULE_1__);
|
|
/* eslint-env mozilla/frame-script */
|
|
|
|
|
|
|
|
|
|
const MERGE_STORE_ACTION = "NEW_TAB_INITIAL_STATE";
|
|
const OUTGOING_MESSAGE_NAME = "ActivityStream:ContentToMain";
|
|
const INCOMING_MESSAGE_NAME = "ActivityStream:MainToContent";
|
|
const EARLY_QUEUED_ACTIONS = [common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].SAVE_SESSION_PERF_DATA, common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].PAGE_PRERENDERED];
|
|
|
|
/**
|
|
* A higher-order function which returns a reducer that, on MERGE_STORE action,
|
|
* will return the action.data object merged into the previous state.
|
|
*
|
|
* For all other actions, it merely calls mainReducer.
|
|
*
|
|
* Because we want this to merge the entire state object, it's written as a
|
|
* higher order function which takes the main reducer (itself often a call to
|
|
* combineReducers) as a parameter.
|
|
*
|
|
* @param {function} mainReducer reducer to call if action != MERGE_STORE_ACTION
|
|
* @return {function} a reducer that, on MERGE_STORE_ACTION action,
|
|
* will return the action.data object merged
|
|
* into the previous state, and the result
|
|
* of calling mainReducer otherwise.
|
|
*/
|
|
function mergeStateReducer(mainReducer) {
|
|
return (prevState, action) => {
|
|
if (action.type === MERGE_STORE_ACTION) {
|
|
return Object.assign({}, prevState, action.data);
|
|
}
|
|
|
|
return mainReducer(prevState, action);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* messageMiddleware - Middleware that looks for SentToMain type actions, and sends them if necessary
|
|
*/
|
|
const messageMiddleware = store => next => action => {
|
|
const skipLocal = action.meta && action.meta.skipLocal;
|
|
if (common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionUtils"].isSendToMain(action)) {
|
|
RPMSendAsyncMessage(OUTGOING_MESSAGE_NAME, action);
|
|
}
|
|
if (!skipLocal) {
|
|
next(action);
|
|
}
|
|
};
|
|
|
|
const rehydrationMiddleware = store => next => action => {
|
|
if (store._didRehydrate) {
|
|
return next(action);
|
|
}
|
|
|
|
const isMergeStoreAction = action.type === MERGE_STORE_ACTION;
|
|
const isRehydrationRequest = action.type === common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].NEW_TAB_STATE_REQUEST;
|
|
|
|
if (isRehydrationRequest) {
|
|
store._didRequestInitialState = true;
|
|
return next(action);
|
|
}
|
|
|
|
if (isMergeStoreAction) {
|
|
store._didRehydrate = true;
|
|
return next(action);
|
|
}
|
|
|
|
// If init happened after our request was made, we need to re-request
|
|
if (store._didRequestInitialState && action.type === common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].INIT) {
|
|
return next(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({ type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].NEW_TAB_STATE_REQUEST }));
|
|
}
|
|
|
|
if (common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionUtils"].isBroadcastToContent(action) || common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionUtils"].isSendToOneContent(action) || common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionUtils"].isSendToPreloaded(action)) {
|
|
// Note that actions received before didRehydrate will not be dispatched
|
|
// because this could negatively affect preloading and the the state
|
|
// will be replaced by rehydration anyway.
|
|
return null;
|
|
}
|
|
|
|
return next(action);
|
|
};
|
|
|
|
/**
|
|
* This middleware queues up all the EARLY_QUEUED_ACTIONS until it receives
|
|
* the first action from main. This is useful for those actions for main which
|
|
* require higher reliability, i.e. the action will not be lost in the case
|
|
* that it gets sent before the main is ready to receive it. Conversely, any
|
|
* actions allowed early are accepted to be ignorable or re-sendable.
|
|
*/
|
|
const queueEarlyMessageMiddleware = store => next => action => {
|
|
if (store._receivedFromMain) {
|
|
next(action);
|
|
} else if (common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionUtils"].isFromMain(action)) {
|
|
next(action);
|
|
store._receivedFromMain = true;
|
|
// Sending out all the early actions as main is ready now
|
|
if (store._earlyActionQueue) {
|
|
store._earlyActionQueue.forEach(next);
|
|
store._earlyActionQueue = [];
|
|
}
|
|
} else if (EARLY_QUEUED_ACTIONS.includes(action.type)) {
|
|
store._earlyActionQueue = store._earlyActionQueue || [];
|
|
store._earlyActionQueue.push(action);
|
|
} else {
|
|
// Let any other type of action go through
|
|
next(action);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* initStore - Create a store and listen for incoming actions
|
|
*
|
|
* @param {object} reducers An object containing Redux reducers
|
|
* @param {object} intialState (optional) The initial state of the store, if desired
|
|
* @return {object} A redux store
|
|
*/
|
|
function initStore(reducers, initialState) {
|
|
const store = Object(redux__WEBPACK_IMPORTED_MODULE_1__["createStore"])(mergeStateReducer(Object(redux__WEBPACK_IMPORTED_MODULE_1__["combineReducers"])(reducers)), initialState, global.RPMAddMessageListener && Object(redux__WEBPACK_IMPORTED_MODULE_1__["applyMiddleware"])(rehydrationMiddleware, queueEarlyMessageMiddleware, messageMiddleware));
|
|
|
|
store._didRehydrate = false;
|
|
store._didRequestInitialState = false;
|
|
|
|
if (global.RPMAddMessageListener) {
|
|
global.RPMAddMessageListener(INCOMING_MESSAGE_NAME, msg => {
|
|
try {
|
|
store.dispatch(msg.data);
|
|
} catch (ex) {
|
|
console.error("Content msg:", msg, "Dispatch error: ", ex); // eslint-disable-line no-console
|
|
dump(`Content msg: ${JSON.stringify(msg)}\nDispatch error: ${ex}\n${ex.stack}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
return store;
|
|
}
|
|
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
|
|
|
|
/***/ }),
|
|
/* 6 */
|
|
/***/ (function(module, exports) {
|
|
|
|
module.exports = Redux;
|
|
|
|
/***/ }),
|
|
/* 7 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "RICH_TEXT_KEYS", function() { return RICH_TEXT_KEYS; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "generateMessages", function() { return generateMessages; });
|
|
/* harmony import */ var fluent__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(43);
|
|
|
|
|
|
/**
|
|
* Properties that allow rich text MUST be added to this list.
|
|
* key: the localization_id that should be used
|
|
* value: a property or array of properties on the message.content object
|
|
*/
|
|
const RICH_TEXT_CONFIG = {
|
|
"text": ["text", "scene1_text"],
|
|
"privacy_html": "scene2_privacy_html",
|
|
"disclaimer_html": "scene2_disclaimer_html"
|
|
};
|
|
|
|
const RICH_TEXT_KEYS = Object.keys(RICH_TEXT_CONFIG);
|
|
|
|
/**
|
|
* Generates an array of messages suitable for fluent's localization provider
|
|
* including all needed strings for rich text.
|
|
* @param {object} content A .content object from an ASR message (i.e. message.content)
|
|
* @returns {MessageContext[]} A array containing the fluent message context
|
|
*/
|
|
function generateMessages(content) {
|
|
const cx = new fluent__WEBPACK_IMPORTED_MODULE_0__["MessageContext"]("en-US");
|
|
|
|
RICH_TEXT_KEYS.forEach(key => {
|
|
const attrs = RICH_TEXT_CONFIG[key];
|
|
const attrsToTry = Array.isArray(attrs) ? [...attrs] : [attrs];
|
|
let string = "";
|
|
while (!string && attrsToTry.length) {
|
|
const attr = attrsToTry.pop();
|
|
string = content[attr];
|
|
}
|
|
cx.addMessages(`${key} = ${string}`);
|
|
});
|
|
return [cx];
|
|
}
|
|
|
|
/***/ }),
|
|
/* 8 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "VISIBLE", function() { return VISIBLE; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "VISIBILITY_CHANGE_EVENT", function() { return VISIBILITY_CHANGE_EVENT; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ImpressionsWrapper", function() { return ImpressionsWrapper; });
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(9);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
|
|
|
|
|
|
const VISIBLE = "visible";
|
|
const VISIBILITY_CHANGE_EVENT = "visibilitychange";
|
|
|
|
/**
|
|
* Component wrapper used to send telemetry pings on every impression.
|
|
*/
|
|
class ImpressionsWrapper extends react__WEBPACK_IMPORTED_MODULE_0___default.a.PureComponent {
|
|
// This sends an event when a user sees a set of new content. If content
|
|
// changes while the page is hidden (i.e. preloaded or on a hidden tab),
|
|
// only send the event if the page becomes visible again.
|
|
sendImpressionOrAddListener() {
|
|
if (this.props.document.visibilityState === VISIBLE) {
|
|
this.props.sendImpression({ id: this.props.id });
|
|
} else {
|
|
// We should only ever send the latest impression stats ping, so remove any
|
|
// older listeners.
|
|
if (this._onVisibilityChange) {
|
|
this.props.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
|
|
}
|
|
|
|
// When the page becomes visible, send the impression stats ping if the section isn't collapsed.
|
|
this._onVisibilityChange = () => {
|
|
if (this.props.document.visibilityState === VISIBLE) {
|
|
this.props.sendImpression({ id: this.props.id });
|
|
this.props.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
|
|
}
|
|
};
|
|
this.props.document.addEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
if (this._onVisibilityChange) {
|
|
this.props.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
|
|
}
|
|
}
|
|
|
|
componentDidMount() {
|
|
if (this.props.sendOnMount) {
|
|
this.sendImpressionOrAddListener();
|
|
}
|
|
}
|
|
|
|
componentDidUpdate(prevProps) {
|
|
if (this.props.shouldSendImpressionOnUpdate(this.props, prevProps)) {
|
|
this.sendImpressionOrAddListener();
|
|
}
|
|
}
|
|
|
|
render() {
|
|
return this.props.children;
|
|
}
|
|
}
|
|
|
|
ImpressionsWrapper.defaultProps = {
|
|
document: global.document,
|
|
sendOnMount: true
|
|
};
|
|
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
|
|
|
|
/***/ }),
|
|
/* 9 */
|
|
/***/ (function(module, exports) {
|
|
|
|
module.exports = React;
|
|
|
|
/***/ }),
|
|
/* 10 */
|
|
/***/ (function(module, exports) {
|
|
|
|
module.exports = PropTypes;
|
|
|
|
/***/ }),
|
|
/* 11 */
|
|
/***/ (function(module, exports) {
|
|
|
|
module.exports = ReactDOM;
|
|
|
|
/***/ }),
|
|
/* 12 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_Base", function() { return _Base; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "BaseContent", function() { return BaseContent; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Base", function() { return Base; });
|
|
/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(13);
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_1__);
|
|
/* harmony import */ var content_src_components_ASRouterAdmin_ASRouterAdmin__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(14);
|
|
/* harmony import */ var content_src_components_ConfirmDialog_ConfirmDialog__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(15);
|
|
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(16);
|
|
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_4__);
|
|
/* harmony import */ var content_src_components_ErrorBoundary_ErrorBoundary__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(17);
|
|
/* harmony import */ var content_src_components_ManualMigration_ManualMigration__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(18);
|
|
/* harmony import */ var common_PrerenderData_jsm__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(19);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(9);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_8___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_8__);
|
|
/* harmony import */ var content_src_components_Search_Search__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(20);
|
|
/* harmony import */ var content_src_components_Sections_Sections__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(22);
|
|
/* harmony import */ var content_src_components_StartupOverlay_StartupOverlay__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(39);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const PrefsButton = Object(react_intl__WEBPACK_IMPORTED_MODULE_1__["injectIntl"])(props => react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(
|
|
"div",
|
|
{ className: "prefs-button" },
|
|
react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement("button", { className: "icon icon-settings", onClick: props.onClick, title: props.intl.formatMessage({ id: "settings_pane_button_label" }) })
|
|
));
|
|
|
|
// Add the locale data for pluralization and relative-time formatting for now,
|
|
// this just uses english locale data. We can make this more sophisticated if
|
|
// more features are needed.
|
|
function addLocaleDataForReactIntl(locale) {
|
|
Object(react_intl__WEBPACK_IMPORTED_MODULE_1__["addLocaleData"])([{ locale, parentLocale: "en" }]);
|
|
}
|
|
|
|
// Returns a function will not be continuously triggered when called. The
|
|
// function will be triggered if called again after `wait` milliseconds.
|
|
function debounce(func, wait) {
|
|
let timer;
|
|
return (...args) => {
|
|
if (timer) {
|
|
return;
|
|
}
|
|
|
|
let wakeUp = () => {
|
|
timer = null;
|
|
};
|
|
|
|
timer = setTimeout(wakeUp, wait);
|
|
func.apply(this, args);
|
|
};
|
|
}
|
|
|
|
class _Base extends react__WEBPACK_IMPORTED_MODULE_8___default.a.PureComponent {
|
|
componentWillMount() {
|
|
const { locale } = this.props;
|
|
addLocaleDataForReactIntl(locale);
|
|
if (this.props.isFirstrun) {
|
|
global.document.body.classList.add("welcome", "hide-main");
|
|
}
|
|
}
|
|
|
|
componentDidMount() {
|
|
// Request state AFTER the first render to ensure we don't cause the
|
|
// prerendered DOM to be unmounted. Otherwise, NEW_TAB_STATE_REQUEST is
|
|
// dispatched right after the store is ready.
|
|
if (this.props.isPrerendered) {
|
|
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({ type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].NEW_TAB_STATE_REQUEST }));
|
|
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({ type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].PAGE_PRERENDERED }));
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
this.updateTheme();
|
|
}
|
|
|
|
componentWillUpdate() {
|
|
this.updateTheme();
|
|
}
|
|
|
|
updateTheme() {
|
|
const bodyClassName = ["activity-stream",
|
|
// If we skipped the about:welcome overlay and removed the CSS classes
|
|
// we don't want to add them back to the Activity Stream view
|
|
document.body.classList.contains("welcome") ? "welcome" : "", document.body.classList.contains("hide-main") ? "hide-main" : ""].filter(v => v).join(" ");
|
|
global.document.body.className = bodyClassName;
|
|
}
|
|
|
|
render() {
|
|
const { props } = this;
|
|
const { App, locale, strings } = props;
|
|
const { initialized } = App;
|
|
|
|
const prefs = props.Prefs.values;
|
|
if (prefs["asrouter.devtoolsEnabled"]) {
|
|
if (window.location.hash === "#asrouter") {
|
|
return react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_ASRouterAdmin_ASRouterAdmin__WEBPACK_IMPORTED_MODULE_2__["ASRouterAdmin"], null);
|
|
}
|
|
console.log("ASRouter devtools enabled. To access visit %cabout:newtab#asrouter", "font-weight: bold"); // eslint-disable-line no-console
|
|
}
|
|
|
|
if (!props.isPrerendered && !initialized) {
|
|
return null;
|
|
}
|
|
|
|
return react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(
|
|
react_intl__WEBPACK_IMPORTED_MODULE_1__["IntlProvider"],
|
|
{ locale: locale, messages: strings },
|
|
react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(
|
|
content_src_components_ErrorBoundary_ErrorBoundary__WEBPACK_IMPORTED_MODULE_5__["ErrorBoundary"],
|
|
{ className: "base-content-fallback" },
|
|
react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(BaseContent, this.props)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
class BaseContent extends react__WEBPACK_IMPORTED_MODULE_8___default.a.PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
this.openPreferences = this.openPreferences.bind(this);
|
|
this.onWindowScroll = debounce(this.onWindowScroll.bind(this), 5);
|
|
this.state = { fixedSearch: false };
|
|
}
|
|
|
|
componentDidMount() {
|
|
global.addEventListener("scroll", this.onWindowScroll);
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
global.removeEventListener("scroll", this.onWindowScroll);
|
|
}
|
|
|
|
onWindowScroll() {
|
|
const SCROLL_THRESHOLD = 34;
|
|
if (global.scrollY > SCROLL_THRESHOLD && !this.state.fixedSearch) {
|
|
this.setState({ fixedSearch: true });
|
|
} else if (global.scrollY <= SCROLL_THRESHOLD && this.state.fixedSearch) {
|
|
this.setState({ fixedSearch: false });
|
|
}
|
|
}
|
|
|
|
openPreferences() {
|
|
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({ type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].SETTINGS_OPEN }));
|
|
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent({ event: "OPEN_NEWTAB_PREFS" }));
|
|
}
|
|
|
|
render() {
|
|
const { props } = this;
|
|
const { App } = props;
|
|
const { initialized } = App;
|
|
const prefs = props.Prefs.values;
|
|
|
|
const shouldBeFixedToTop = common_PrerenderData_jsm__WEBPACK_IMPORTED_MODULE_7__["PrerenderData"].arePrefsValid(name => prefs[name]);
|
|
const noSectionsEnabled = !prefs["feeds.topsites"] && props.Sections.filter(section => section.enabled).length === 0;
|
|
|
|
const outerClassName = ["outer-wrapper", shouldBeFixedToTop && "fixed-to-top", prefs.showSearch && this.state.fixedSearch && !noSectionsEnabled && "fixed-search", prefs.showSearch && noSectionsEnabled && "only-search"].filter(v => v).join(" ");
|
|
|
|
return react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(
|
|
"div",
|
|
null,
|
|
react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(
|
|
"div",
|
|
{ className: outerClassName },
|
|
react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(
|
|
"main",
|
|
null,
|
|
prefs.showSearch && react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(
|
|
"div",
|
|
{ className: "non-collapsible-section" },
|
|
react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(
|
|
content_src_components_ErrorBoundary_ErrorBoundary__WEBPACK_IMPORTED_MODULE_5__["ErrorBoundary"],
|
|
null,
|
|
react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_Search_Search__WEBPACK_IMPORTED_MODULE_9__["Search"], { showLogo: noSectionsEnabled })
|
|
)
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(
|
|
"div",
|
|
{ className: `body-wrapper${initialized ? " on" : ""}` },
|
|
!prefs.migrationExpired && react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(
|
|
"div",
|
|
{ className: "non-collapsible-section" },
|
|
react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_ManualMigration_ManualMigration__WEBPACK_IMPORTED_MODULE_6__["ManualMigration"], null)
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_Sections_Sections__WEBPACK_IMPORTED_MODULE_10__["Sections"], null),
|
|
react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(PrefsButton, { onClick: this.openPreferences })
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_ConfirmDialog_ConfirmDialog__WEBPACK_IMPORTED_MODULE_3__["ConfirmDialog"], null)
|
|
)
|
|
),
|
|
this.props.isFirstrun && react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_StartupOverlay_StartupOverlay__WEBPACK_IMPORTED_MODULE_11__["StartupOverlay"], null)
|
|
);
|
|
}
|
|
}
|
|
|
|
const Base = Object(react_redux__WEBPACK_IMPORTED_MODULE_4__["connect"])(state => ({ App: state.App, Prefs: state.Prefs, Sections: state.Sections }))(_Base);
|
|
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
|
|
|
|
/***/ }),
|
|
/* 13 */
|
|
/***/ (function(module, exports) {
|
|
|
|
module.exports = ReactIntl;
|
|
|
|
/***/ }),
|
|
/* 14 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ASRouterAdmin", function() { return ASRouterAdmin; });
|
|
/* harmony import */ var _asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(9);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__);
|
|
|
|
|
|
|
|
class ASRouterAdmin extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
this.onMessage = this.onMessage.bind(this);
|
|
this.handleEnabledToggle = this.handleEnabledToggle.bind(this);
|
|
this.handleUserPrefToggle = this.handleUserPrefToggle.bind(this);
|
|
this.onChangeMessageFilter = this.onChangeMessageFilter.bind(this);
|
|
this.findOtherBundledMessagesOfSameTemplate = this.findOtherBundledMessagesOfSameTemplate.bind(this);
|
|
this.handleExpressionEval = this.handleExpressionEval.bind(this);
|
|
this.onChangeTargetingParameters = this.onChangeTargetingParameters.bind(this);
|
|
this.state = { messageFilter: "all", evaluationStatus: {}, stringTargetingParameters: null };
|
|
}
|
|
|
|
onMessage({ data: action }) {
|
|
if (action.type === "ADMIN_SET_STATE") {
|
|
this.setState(action.data);
|
|
if (!this.state.stringTargetingParameters) {
|
|
const stringTargetingParameters = {};
|
|
for (const param of Object.keys(action.data.targetingParameters)) {
|
|
stringTargetingParameters[param] = JSON.stringify(action.data.targetingParameters[param], null, 2);
|
|
}
|
|
this.setState({ stringTargetingParameters });
|
|
}
|
|
}
|
|
}
|
|
|
|
componentWillMount() {
|
|
const endpoint = _asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_0__["ASRouterUtils"].getPreviewEndpoint();
|
|
_asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_0__["ASRouterUtils"].sendMessage({ type: "ADMIN_CONNECT_STATE", data: { endpoint } });
|
|
_asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_0__["ASRouterUtils"].addListener(this.onMessage);
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
_asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_0__["ASRouterUtils"].removeListener(this.onMessage);
|
|
}
|
|
|
|
findOtherBundledMessagesOfSameTemplate(template) {
|
|
return this.state.messages.filter(msg => msg.template === template && msg.bundled);
|
|
}
|
|
|
|
handleBlock(msg) {
|
|
if (msg.bundled) {
|
|
// If we are blocking a message that belongs to a bundle, block all other messages that are bundled of that same template
|
|
let bundle = this.findOtherBundledMessagesOfSameTemplate(msg.template);
|
|
return () => _asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_0__["ASRouterUtils"].blockBundle(bundle);
|
|
}
|
|
return () => _asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_0__["ASRouterUtils"].blockById(msg.id);
|
|
}
|
|
|
|
handleUnblock(msg) {
|
|
if (msg.bundled) {
|
|
// If we are unblocking a message that belongs to a bundle, unblock all other messages that are bundled of that same template
|
|
let bundle = this.findOtherBundledMessagesOfSameTemplate(msg.template);
|
|
return () => _asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_0__["ASRouterUtils"].unblockBundle(bundle);
|
|
}
|
|
return () => _asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_0__["ASRouterUtils"].unblockById(msg.id);
|
|
}
|
|
|
|
handleOverride(id) {
|
|
return () => _asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_0__["ASRouterUtils"].overrideMessage(id);
|
|
}
|
|
|
|
expireCache() {
|
|
_asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_0__["ASRouterUtils"].sendMessage({ type: "EXPIRE_QUERY_CACHE" });
|
|
}
|
|
|
|
resetPref() {
|
|
_asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_0__["ASRouterUtils"].sendMessage({ type: "RESET_PROVIDER_PREF" });
|
|
}
|
|
|
|
handleExpressionEval() {
|
|
const context = {};
|
|
for (const param of Object.keys(this.state.stringTargetingParameters)) {
|
|
const value = this.state.stringTargetingParameters[param];
|
|
context[param] = value ? JSON.parse(value) : null;
|
|
}
|
|
_asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_0__["ASRouterUtils"].sendMessage({
|
|
type: "EVALUATE_JEXL_EXPRESSION",
|
|
data: {
|
|
expression: this.refs.expressionInput.value,
|
|
context
|
|
}
|
|
});
|
|
}
|
|
|
|
onChangeTargetingParameters(event) {
|
|
const { name } = event.target;
|
|
const { value } = event.target;
|
|
this.refs.evaluationStatus.innerText = "";
|
|
|
|
this.setState(({ stringTargetingParameters }) => {
|
|
let targetingParametersError = null;
|
|
const updatedParameters = Object.assign({}, stringTargetingParameters);
|
|
updatedParameters[name] = value;
|
|
try {
|
|
JSON.parse(value);
|
|
} catch (e) {
|
|
console.log(`Error parsing value of parameter ${name}`); // eslint-disable-line no-console
|
|
targetingParametersError = { id: name };
|
|
}
|
|
|
|
return { stringTargetingParameters: updatedParameters, targetingParametersError };
|
|
});
|
|
}
|
|
|
|
renderMessageItem(msg) {
|
|
const isCurrent = msg.id === this.state.lastMessageId;
|
|
const isBlocked = this.state.messageBlockList.includes(msg.id);
|
|
const impressions = this.state.messageImpressions[msg.id] ? this.state.messageImpressions[msg.id].length : 0;
|
|
|
|
let itemClassName = "message-item";
|
|
if (isCurrent) {
|
|
itemClassName += " current";
|
|
}
|
|
if (isBlocked) {
|
|
itemClassName += " blocked";
|
|
}
|
|
|
|
return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"tr",
|
|
{ className: itemClassName, key: msg.id },
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"td",
|
|
{ className: "message-id" },
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"span",
|
|
null,
|
|
msg.id,
|
|
" ",
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("br", null)
|
|
)
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"td",
|
|
null,
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"button",
|
|
{ className: `button ${isBlocked ? "" : " primary"}`, onClick: isBlocked ? this.handleUnblock(msg) : this.handleBlock(msg) },
|
|
isBlocked ? "Unblock" : "Block"
|
|
),
|
|
isBlocked ? null : react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"button",
|
|
{ className: "button", onClick: this.handleOverride(msg.id) },
|
|
"Show"
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("br", null),
|
|
"(",
|
|
impressions,
|
|
" impressions)"
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"td",
|
|
{ className: "message-summary" },
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"pre",
|
|
null,
|
|
JSON.stringify(msg, null, 2)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
renderMessages() {
|
|
if (!this.state.messages) {
|
|
return null;
|
|
}
|
|
const messagesToShow = this.state.messageFilter === "all" ? this.state.messages : this.state.messages.filter(message => message.provider === this.state.messageFilter);
|
|
return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"table",
|
|
null,
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"tbody",
|
|
null,
|
|
messagesToShow.map(msg => this.renderMessageItem(msg))
|
|
)
|
|
);
|
|
}
|
|
|
|
onChangeMessageFilter(event) {
|
|
this.setState({ messageFilter: event.target.value });
|
|
}
|
|
|
|
renderMessageFilter() {
|
|
if (!this.state.providers) {
|
|
return null;
|
|
}
|
|
return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"p",
|
|
null,
|
|
"Show messages from ",
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"select",
|
|
{ value: this.state.messageFilter, onChange: this.onChangeMessageFilter },
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"option",
|
|
{ value: "all" },
|
|
"all providers"
|
|
),
|
|
this.state.providers.map(provider => react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"option",
|
|
{ key: provider.id, value: provider.id },
|
|
provider.id
|
|
))
|
|
)
|
|
);
|
|
}
|
|
|
|
renderTableHead() {
|
|
return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"thead",
|
|
null,
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"tr",
|
|
{ className: "message-item" },
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("td", { className: "min" }),
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"td",
|
|
{ className: "min" },
|
|
"Provider ID"
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"td",
|
|
null,
|
|
"Source"
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"td",
|
|
null,
|
|
"Last Updated"
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
handleEnabledToggle(event) {
|
|
const provider = this.state.providerPrefs.find(p => p.id === event.target.dataset.provider);
|
|
const userPrefInfo = this.state.userPrefs;
|
|
|
|
const isUserEnabled = provider.id in userPrefInfo ? userPrefInfo[provider.id] : true;
|
|
const isSystemEnabled = provider.enabled;
|
|
const isEnabling = event.target.checked;
|
|
|
|
if (isEnabling) {
|
|
if (!isUserEnabled) {
|
|
_asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_0__["ASRouterUtils"].sendMessage({ type: "SET_PROVIDER_USER_PREF", data: { id: provider.id, value: true } });
|
|
}
|
|
if (!isSystemEnabled) {
|
|
_asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_0__["ASRouterUtils"].sendMessage({ type: "ENABLE_PROVIDER", data: provider.id });
|
|
}
|
|
} else {
|
|
_asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_0__["ASRouterUtils"].sendMessage({ type: "DISABLE_PROVIDER", data: provider.id });
|
|
}
|
|
|
|
this.setState({ messageFilter: "all" });
|
|
}
|
|
|
|
handleUserPrefToggle(event) {
|
|
const action = { type: "SET_PROVIDER_USER_PREF", data: { id: event.target.dataset.provider, value: event.target.checked } };
|
|
_asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_0__["ASRouterUtils"].sendMessage(action);
|
|
this.setState({ messageFilter: "all" });
|
|
}
|
|
|
|
renderProviders() {
|
|
const providersConfig = this.state.providerPrefs;
|
|
const providerInfo = this.state.providers;
|
|
const userPrefInfo = this.state.userPrefs;
|
|
|
|
return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"table",
|
|
null,
|
|
this.renderTableHead(),
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"tbody",
|
|
null,
|
|
providersConfig.map((provider, i) => {
|
|
const isTestProvider = provider.id === "snippets_local_testing";
|
|
const info = providerInfo.find(p => p.id === provider.id) || {};
|
|
const isUserEnabled = provider.id in userPrefInfo ? userPrefInfo[provider.id] : true;
|
|
const isSystemEnabled = isTestProvider || provider.enabled;
|
|
|
|
let label = "local";
|
|
if (provider.type === "remote") {
|
|
let displayUrl = "";
|
|
try {
|
|
displayUrl = `(${new URL(info.url).hostname})`;
|
|
} catch (err) {}
|
|
label = react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"span",
|
|
null,
|
|
"endpoint ",
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"a",
|
|
{ target: "_blank", href: info.url },
|
|
displayUrl
|
|
)
|
|
);
|
|
} else if (provider.type === "remote-settings") {
|
|
label = `remote settings (${provider.bucket})`;
|
|
}
|
|
|
|
let reasonsDisabled = [];
|
|
if (!isSystemEnabled) {
|
|
reasonsDisabled.push("system pref");
|
|
}
|
|
if (!isUserEnabled) {
|
|
reasonsDisabled.push("user pref");
|
|
}
|
|
if (reasonsDisabled.length) {
|
|
label = `disabled via ${reasonsDisabled.join(", ")}`;
|
|
}
|
|
|
|
return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"tr",
|
|
{ className: "message-item", key: i },
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"td",
|
|
null,
|
|
isTestProvider ? react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("input", { type: "checkbox", disabled: true, readOnly: true, checked: true }) : react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("input", { type: "checkbox", "data-provider": provider.id, checked: isUserEnabled && isSystemEnabled, onChange: this.handleEnabledToggle })
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"td",
|
|
null,
|
|
provider.id
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"td",
|
|
null,
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"span",
|
|
{ className: `sourceLabel${isUserEnabled && isSystemEnabled ? "" : " isDisabled"}` },
|
|
label
|
|
)
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"td",
|
|
{ style: { whiteSpace: "nowrap" } },
|
|
info.lastUpdated ? new Date(info.lastUpdated).toLocaleString() : ""
|
|
)
|
|
);
|
|
})
|
|
)
|
|
);
|
|
}
|
|
|
|
renderTargetingParameters() {
|
|
// There was no error and the result is truthy
|
|
const success = this.state.evaluationStatus.success && !!this.state.evaluationStatus.result;
|
|
const result = JSON.stringify(this.state.evaluationStatus.result, null, 2) || "(Empty result)";
|
|
|
|
return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"table",
|
|
null,
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"tbody",
|
|
null,
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"tr",
|
|
null,
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"td",
|
|
null,
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"h2",
|
|
null,
|
|
"Evaluate JEXL expression"
|
|
)
|
|
)
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"tr",
|
|
null,
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"td",
|
|
null,
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"p",
|
|
null,
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("textarea", { ref: "expressionInput", rows: "10", cols: "60", placeholder: "Evaluate JEXL expressions and mock parameters by changing their values below" })
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"p",
|
|
null,
|
|
"Status: ",
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"span",
|
|
{ ref: "evaluationStatus" },
|
|
success ? "✅" : "❌",
|
|
", Result: ",
|
|
result
|
|
)
|
|
)
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"td",
|
|
null,
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"button",
|
|
{ className: "ASRouterButton secondary", onClick: this.handleExpressionEval },
|
|
"Evaluate"
|
|
)
|
|
)
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"tr",
|
|
null,
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"td",
|
|
null,
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"h2",
|
|
null,
|
|
"Modify targeting parameters"
|
|
)
|
|
)
|
|
),
|
|
this.state.stringTargetingParameters && Object.keys(this.state.stringTargetingParameters).map((param, i) => {
|
|
const value = this.state.stringTargetingParameters[param];
|
|
const errorState = this.state.targetingParametersError && this.state.targetingParametersError.id === param;
|
|
const className = errorState ? "errorState" : "";
|
|
const inputComp = (value && value.length) > 30 ? react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("textarea", { name: param, className: className, value: value, rows: "10", cols: "60", onChange: this.onChangeTargetingParameters }) : react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("input", { name: param, className: className, value: value, onChange: this.onChangeTargetingParameters });
|
|
|
|
return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"tr",
|
|
{ key: i },
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"td",
|
|
null,
|
|
param
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"td",
|
|
null,
|
|
inputComp
|
|
)
|
|
);
|
|
})
|
|
)
|
|
);
|
|
}
|
|
|
|
render() {
|
|
return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"div",
|
|
{ className: "asrouter-admin outer-wrapper" },
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"h1",
|
|
null,
|
|
"AS Router Admin"
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"h2",
|
|
null,
|
|
"Targeting Utilities"
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"button",
|
|
{ className: "button", onClick: this.expireCache },
|
|
"Expire Cache"
|
|
),
|
|
" (This expires the cache in ASR Targeting for bookmarks and top sites)",
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"h2",
|
|
null,
|
|
"Message Providers ",
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"button",
|
|
{ title: "Restore all provider settings that ship with Firefox", className: "button", onClick: this.resetPref },
|
|
"Restore default prefs"
|
|
)
|
|
),
|
|
this.state.providers ? this.renderProviders() : null,
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"h2",
|
|
null,
|
|
"Messages"
|
|
),
|
|
this.renderMessageFilter(),
|
|
this.renderMessages(),
|
|
this.renderTargetingParameters()
|
|
);
|
|
}
|
|
}
|
|
|
|
/***/ }),
|
|
/* 15 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_ConfirmDialog", function() { return _ConfirmDialog; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ConfirmDialog", function() { return ConfirmDialog; });
|
|
/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
|
|
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(16);
|
|
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_1__);
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(13);
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_2__);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(9);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_3__);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
* ConfirmDialog component.
|
|
* One primary action button, one cancel button.
|
|
*
|
|
* Content displayed is controlled by `data` prop the component receives.
|
|
* Example:
|
|
* data: {
|
|
* // Any sort of data needed to be passed around by actions.
|
|
* payload: site.url,
|
|
* // Primary button AlsoToMain action.
|
|
* action: "DELETE_HISTORY_URL",
|
|
* // Primary button USerEvent action.
|
|
* userEvent: "DELETE",
|
|
* // Array of locale ids to display.
|
|
* message_body: ["confirm_history_delete_p1", "confirm_history_delete_notice_p2"],
|
|
* // Text for primary button.
|
|
* confirm_button_string_id: "menu_action_delete"
|
|
* },
|
|
*/
|
|
class _ConfirmDialog extends react__WEBPACK_IMPORTED_MODULE_3___default.a.PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
this._handleCancelBtn = this._handleCancelBtn.bind(this);
|
|
this._handleConfirmBtn = this._handleConfirmBtn.bind(this);
|
|
}
|
|
|
|
_handleCancelBtn() {
|
|
this.props.dispatch({ type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].DIALOG_CANCEL });
|
|
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent({ event: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].DIALOG_CANCEL, source: this.props.data.eventSource }));
|
|
}
|
|
|
|
_handleConfirmBtn() {
|
|
this.props.data.onConfirm.forEach(this.props.dispatch);
|
|
}
|
|
|
|
_renderModalMessage() {
|
|
const message_body = this.props.data.body_string_id;
|
|
|
|
if (!message_body) {
|
|
return null;
|
|
}
|
|
|
|
return react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"span",
|
|
null,
|
|
message_body.map(msg => react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"p",
|
|
{ key: msg },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_2__["FormattedMessage"], { id: msg })
|
|
))
|
|
);
|
|
}
|
|
|
|
render() {
|
|
if (!this.props.visible) {
|
|
return null;
|
|
}
|
|
|
|
return react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"div",
|
|
{ className: "confirmation-dialog" },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("div", { className: "modal-overlay", onClick: this._handleCancelBtn }),
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"div",
|
|
{ className: "modal" },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"section",
|
|
{ className: "modal-message" },
|
|
this.props.data.icon && react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("span", { className: `icon icon-spacer icon-${this.props.data.icon}` }),
|
|
this._renderModalMessage()
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"section",
|
|
{ className: "actions" },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"button",
|
|
{ onClick: this._handleCancelBtn },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_2__["FormattedMessage"], { id: this.props.data.cancel_button_string_id })
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"button",
|
|
{ className: "done", onClick: this._handleConfirmBtn },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_2__["FormattedMessage"], { id: this.props.data.confirm_button_string_id })
|
|
)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
const ConfirmDialog = Object(react_redux__WEBPACK_IMPORTED_MODULE_1__["connect"])(state => state.Dialog)(_ConfirmDialog);
|
|
|
|
/***/ }),
|
|
/* 16 */
|
|
/***/ (function(module, exports) {
|
|
|
|
module.exports = ReactRedux;
|
|
|
|
/***/ }),
|
|
/* 17 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ErrorBoundaryFallback", function() { return ErrorBoundaryFallback; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ErrorBoundary", function() { return ErrorBoundary; });
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(13);
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_0__);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(9);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__);
|
|
|
|
|
|
|
|
class ErrorBoundaryFallback extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
this.windowObj = this.props.windowObj || window;
|
|
this.onClick = this.onClick.bind(this);
|
|
}
|
|
|
|
/**
|
|
* Since we only get here if part of the page has crashed, do a
|
|
* forced reload to give us the best chance at recovering.
|
|
*/
|
|
onClick() {
|
|
this.windowObj.location.reload(true);
|
|
}
|
|
|
|
render() {
|
|
const defaultClass = "as-error-fallback";
|
|
let className;
|
|
if ("className" in this.props) {
|
|
className = `${this.props.className} ${defaultClass}`;
|
|
} else {
|
|
className = defaultClass;
|
|
}
|
|
|
|
// href="#" to force normal link styling stuff (eg cursor on hover)
|
|
return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"div",
|
|
{ className: className },
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"div",
|
|
null,
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_0__["FormattedMessage"], {
|
|
defaultMessage: "Oops, something went wrong loading this content.",
|
|
id: "error_fallback_default_info" })
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"span",
|
|
null,
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"a",
|
|
{ href: "#", className: "reload-button", onClick: this.onClick },
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_0__["FormattedMessage"], {
|
|
defaultMessage: "Refresh page to try again.",
|
|
id: "error_fallback_default_refresh_suggestion" })
|
|
)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
ErrorBoundaryFallback.defaultProps = { className: "as-error-fallback" };
|
|
|
|
class ErrorBoundary extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = { hasError: false };
|
|
}
|
|
|
|
componentDidCatch(error, info) {
|
|
this.setState({ hasError: true });
|
|
}
|
|
|
|
render() {
|
|
if (!this.state.hasError) {
|
|
return this.props.children;
|
|
}
|
|
|
|
return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(this.props.FallbackComponent, { className: this.props.className });
|
|
}
|
|
}
|
|
|
|
ErrorBoundary.defaultProps = { FallbackComponent: ErrorBoundaryFallback };
|
|
|
|
/***/ }),
|
|
/* 18 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_ManualMigration", function() { return _ManualMigration; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ManualMigration", function() { return ManualMigration; });
|
|
/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
|
|
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(16);
|
|
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_1__);
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(13);
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_2__);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(9);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_3__);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Manual migration component used to start the profile import wizard.
|
|
* Message is presented temporarily and will go away if:
|
|
* 1. User clicks "No Thanks"
|
|
* 2. User completed the data import
|
|
* 3. After 3 active days
|
|
* 4. User clicks "Cancel" on the import wizard (currently not implemented).
|
|
*/
|
|
class _ManualMigration extends react__WEBPACK_IMPORTED_MODULE_3___default.a.PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
this.onLaunchTour = this.onLaunchTour.bind(this);
|
|
this.onCancelTour = this.onCancelTour.bind(this);
|
|
}
|
|
|
|
onLaunchTour() {
|
|
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({ type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].MIGRATION_START }));
|
|
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent({ event: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].MIGRATION_START }));
|
|
}
|
|
|
|
onCancelTour() {
|
|
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({ type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].MIGRATION_CANCEL }));
|
|
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent({ event: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].MIGRATION_CANCEL }));
|
|
}
|
|
|
|
render() {
|
|
return react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"div",
|
|
{ className: "manual-migration-container" },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"p",
|
|
null,
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("span", { className: "icon icon-import" }),
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_2__["FormattedMessage"], { id: "manual_migration_explanation2" })
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"div",
|
|
{ className: "manual-migration-actions actions" },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"button",
|
|
{ className: "dismiss", onClick: this.onCancelTour },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_2__["FormattedMessage"], { id: "manual_migration_cancel_button" })
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"button",
|
|
{ onClick: this.onLaunchTour },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_2__["FormattedMessage"], { id: "manual_migration_import_button" })
|
|
)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
const ManualMigration = Object(react_redux__WEBPACK_IMPORTED_MODULE_1__["connect"])()(_ManualMigration);
|
|
|
|
/***/ }),
|
|
/* 19 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_PrerenderData", function() { return _PrerenderData; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "PrerenderData", function() { return PrerenderData; });
|
|
class _PrerenderData {
|
|
constructor(options) {
|
|
this.initialPrefs = options.initialPrefs;
|
|
this.initialSections = options.initialSections;
|
|
this._setValidation(options.validation);
|
|
}
|
|
|
|
get validation() {
|
|
return this._validation;
|
|
}
|
|
|
|
set validation(value) {
|
|
this._setValidation(value);
|
|
}
|
|
|
|
get invalidatingPrefs() {
|
|
return this._invalidatingPrefs;
|
|
}
|
|
|
|
// This is needed so we can use it in the constructor
|
|
_setValidation(value = []) {
|
|
this._validation = value;
|
|
this._invalidatingPrefs = value.reduce((result, next) => {
|
|
if (typeof next === "string") {
|
|
result.push(next);
|
|
return result;
|
|
} else if (next && next.oneOf) {
|
|
return result.concat(next.oneOf);
|
|
} else if (next && next.indexedDB) {
|
|
return result.concat(next.indexedDB);
|
|
}
|
|
throw new Error("Your validation configuration is not properly configured");
|
|
}, []);
|
|
}
|
|
|
|
arePrefsValid(getPref, indexedDBPrefs) {
|
|
for (const prefs of this.validation) {
|
|
// {oneOf: ["foo", "bar"]}
|
|
if (prefs && prefs.oneOf && !prefs.oneOf.some(name => getPref(name) === this.initialPrefs[name])) {
|
|
return false;
|
|
|
|
// {indexedDB: ["foo", "bar"]}
|
|
} else if (indexedDBPrefs && prefs && prefs.indexedDB) {
|
|
const anyModifiedPrefs = prefs.indexedDB.some(prefName => indexedDBPrefs.some(pref => pref && pref[prefName]));
|
|
if (anyModifiedPrefs) {
|
|
return false;
|
|
}
|
|
// "foo"
|
|
} else if (getPref(prefs) !== this.initialPrefs[prefs]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
var PrerenderData = new _PrerenderData({
|
|
initialPrefs: {
|
|
"migrationExpired": true,
|
|
"feeds.topsites": true,
|
|
"showSearch": true,
|
|
"topSitesRows": 1,
|
|
"feeds.section.topstories": true,
|
|
"feeds.section.highlights": true,
|
|
"sectionOrder": "topsites,topstories,highlights",
|
|
"collapsed": false
|
|
},
|
|
// Prefs listed as invalidating will prevent the prerendered version
|
|
// of AS from being used if their value is something other than what is listed
|
|
// here. This is required because some preferences cause the page layout to be
|
|
// too different for the prerendered version to be used. Unfortunately, this
|
|
// will result in users who have modified some of their preferences not being
|
|
// able to get the benefits of prerendering.
|
|
validation: ["feeds.topsites", "showSearch", "topSitesRows", "sectionOrder",
|
|
// This means if either of these are set to their default values,
|
|
// prerendering can be used.
|
|
{ oneOf: ["feeds.section.topstories", "feeds.section.highlights"] },
|
|
// If any component has the following preference set to `true` it will
|
|
// invalidate the prerendered version.
|
|
{ indexedDB: ["collapsed"] }],
|
|
initialSections: [{
|
|
enabled: true,
|
|
icon: "pocket",
|
|
id: "topstories",
|
|
order: 1,
|
|
title: { id: "header_recommended_by", values: { provider: "Pocket" } }
|
|
}, {
|
|
enabled: true,
|
|
id: "highlights",
|
|
icon: "highlights",
|
|
order: 2,
|
|
title: { id: "header_highlights" }
|
|
}]
|
|
});
|
|
|
|
/***/ }),
|
|
/* 20 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_Search", function() { return _Search; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Search", function() { return Search; });
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(13);
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_0__);
|
|
/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2);
|
|
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(16);
|
|
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_2__);
|
|
/* harmony import */ var content_src_lib_constants__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(21);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(9);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_4__);
|
|
/* globals ContentSearchUIController */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _Search extends react__WEBPACK_IMPORTED_MODULE_4___default.a.PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
this.onClick = this.onClick.bind(this);
|
|
this.onInputMount = this.onInputMount.bind(this);
|
|
}
|
|
|
|
handleEvent(event) {
|
|
// Also track search events with our own telemetry
|
|
if (event.detail.type === "Search") {
|
|
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_1__["actionCreators"].UserEvent({ event: "SEARCH" }));
|
|
}
|
|
}
|
|
|
|
onClick(event) {
|
|
window.gContentSearchController.search(event);
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
delete window.gContentSearchController;
|
|
}
|
|
|
|
onInputMount(input) {
|
|
if (input) {
|
|
// The "healthReportKey" and needs to be "newtab" or "abouthome" so that
|
|
// BrowserUsageTelemetry.jsm knows to handle events with this name, and
|
|
// can add the appropriate telemetry probes for search. Without the correct
|
|
// name, certain tests like browser_UsageTelemetry_content.js will fail
|
|
// (See github ticket #2348 for more details)
|
|
const healthReportKey = content_src_lib_constants__WEBPACK_IMPORTED_MODULE_3__["IS_NEWTAB"] ? "newtab" : "abouthome";
|
|
|
|
// The "searchSource" needs to be "newtab" or "homepage" and is sent with
|
|
// the search data and acts as context for the search request (See
|
|
// nsISearchEngine.getSubmission). It is necessary so that search engine
|
|
// plugins can correctly atribute referrals. (See github ticket #3321 for
|
|
// more details)
|
|
const searchSource = content_src_lib_constants__WEBPACK_IMPORTED_MODULE_3__["IS_NEWTAB"] ? "newtab" : "homepage";
|
|
|
|
// gContentSearchController needs to exist as a global so that tests for
|
|
// the existing about:home can find it; and so it allows these tests to pass.
|
|
// In the future, when activity stream is default about:home, this can be renamed
|
|
window.gContentSearchController = new ContentSearchUIController(input, input.parentNode, healthReportKey, searchSource);
|
|
addEventListener("ContentSearchClient", this);
|
|
} else {
|
|
window.gContentSearchController = null;
|
|
removeEventListener("ContentSearchClient", this);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Do not change the ID on the input field, as legacy newtab code
|
|
* specifically looks for the id 'newtab-search-text' on input fields
|
|
* in order to execute searches in various tests
|
|
*/
|
|
render() {
|
|
return react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(
|
|
"div",
|
|
{ className: "search-wrapper" },
|
|
this.props.showLogo && react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(
|
|
"div",
|
|
{ className: "logo-and-wordmark" },
|
|
react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("div", { className: "logo" }),
|
|
react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("div", { className: "wordmark" })
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(
|
|
"div",
|
|
{ className: "search-inner-wrapper" },
|
|
react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(
|
|
"label",
|
|
{ htmlFor: "newtab-search-text", className: "search-label" },
|
|
react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(
|
|
"span",
|
|
{ className: "sr-only" },
|
|
react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_0__["FormattedMessage"], { id: "search_web_placeholder" })
|
|
)
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("input", {
|
|
id: "newtab-search-text",
|
|
maxLength: "256",
|
|
placeholder: this.props.intl.formatMessage({ id: "search_web_placeholder" }),
|
|
ref: this.onInputMount,
|
|
title: this.props.intl.formatMessage({ id: "search_web_placeholder" }),
|
|
type: "search" }),
|
|
react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(
|
|
"button",
|
|
{
|
|
id: "searchSubmit",
|
|
className: "search-button",
|
|
onClick: this.onClick,
|
|
title: this.props.intl.formatMessage({ id: "search_button" }) },
|
|
react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(
|
|
"span",
|
|
{ className: "sr-only" },
|
|
react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_0__["FormattedMessage"], { id: "search_button" })
|
|
)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
const Search = Object(react_redux__WEBPACK_IMPORTED_MODULE_2__["connect"])()(Object(react_intl__WEBPACK_IMPORTED_MODULE_0__["injectIntl"])(_Search));
|
|
|
|
/***/ }),
|
|
/* 21 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "IS_NEWTAB", function() { return IS_NEWTAB; });
|
|
const IS_NEWTAB = global.document && global.document.documentURI === "about:newtab";
|
|
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
|
|
|
|
/***/ }),
|
|
/* 22 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Section", function() { return Section; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SectionIntl", function() { return SectionIntl; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_Sections", function() { return _Sections; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Sections", function() { return Sections; });
|
|
/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
|
|
/* harmony import */ var content_src_components_Card_Card__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(47);
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(13);
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_2__);
|
|
/* harmony import */ var content_src_components_CollapsibleSection_CollapsibleSection__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(27);
|
|
/* harmony import */ var content_src_components_ComponentPerfTimer_ComponentPerfTimer__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(30);
|
|
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(16);
|
|
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_5__);
|
|
/* harmony import */ var content_src_components_MoreRecommendations_MoreRecommendations__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(32);
|
|
/* harmony import */ var content_src_components_PocketLoggedInCta_PocketLoggedInCta__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(33);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(9);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_8___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_8__);
|
|
/* harmony import */ var content_src_components_Topics_Topics__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(34);
|
|
/* harmony import */ var content_src_components_TopSites_TopSites__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(35);
|
|
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const VISIBLE = "visible";
|
|
const VISIBILITY_CHANGE_EVENT = "visibilitychange";
|
|
const CARDS_PER_ROW_DEFAULT = 3;
|
|
const CARDS_PER_ROW_COMPACT_WIDE = 4;
|
|
|
|
function getFormattedMessage(message) {
|
|
return typeof message === "string" ? react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(
|
|
"span",
|
|
null,
|
|
message
|
|
) : react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_2__["FormattedMessage"], message);
|
|
}
|
|
|
|
class Section extends react__WEBPACK_IMPORTED_MODULE_8___default.a.PureComponent {
|
|
get numRows() {
|
|
const { rowsPref, maxRows, Prefs } = this.props;
|
|
return rowsPref ? Prefs.values[rowsPref] : maxRows;
|
|
}
|
|
|
|
_dispatchImpressionStats() {
|
|
const { props } = this;
|
|
let cardsPerRow = CARDS_PER_ROW_DEFAULT;
|
|
if (props.compactCards && global.matchMedia(`(min-width: 1072px)`).matches) {
|
|
// If the section has compact cards and the viewport is wide enough, we show
|
|
// 4 columns instead of 3.
|
|
// $break-point-widest = 1072px (from _variables.scss)
|
|
cardsPerRow = CARDS_PER_ROW_COMPACT_WIDE;
|
|
}
|
|
const maxCards = cardsPerRow * this.numRows;
|
|
const cards = props.rows.slice(0, maxCards);
|
|
|
|
if (this.needsImpressionStats(cards)) {
|
|
props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].ImpressionStats({
|
|
source: props.eventSource,
|
|
tiles: cards.map(link => ({ id: link.guid }))
|
|
}));
|
|
this.impressionCardGuids = cards.map(link => link.guid);
|
|
}
|
|
}
|
|
|
|
// This sends an event when a user sees a set of new content. If content
|
|
// changes while the page is hidden (i.e. preloaded or on a hidden tab),
|
|
// only send the event if the page becomes visible again.
|
|
sendImpressionStatsOrAddListener() {
|
|
const { props } = this;
|
|
|
|
if (!props.shouldSendImpressionStats || !props.dispatch) {
|
|
return;
|
|
}
|
|
|
|
if (props.document.visibilityState === VISIBLE) {
|
|
this._dispatchImpressionStats();
|
|
} else {
|
|
// We should only ever send the latest impression stats ping, so remove any
|
|
// older listeners.
|
|
if (this._onVisibilityChange) {
|
|
props.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
|
|
}
|
|
|
|
// When the page becomes visible, send the impression stats ping if the section isn't collapsed.
|
|
this._onVisibilityChange = () => {
|
|
if (props.document.visibilityState === VISIBLE) {
|
|
if (!this.props.pref.collapsed) {
|
|
this._dispatchImpressionStats();
|
|
}
|
|
props.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
|
|
}
|
|
};
|
|
props.document.addEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
|
|
}
|
|
}
|
|
|
|
componentWillMount() {
|
|
this.sendNewTabRehydrated(this.props.initialized);
|
|
}
|
|
|
|
componentDidMount() {
|
|
if (this.props.rows.length && !this.props.pref.collapsed) {
|
|
this.sendImpressionStatsOrAddListener();
|
|
}
|
|
}
|
|
|
|
componentDidUpdate(prevProps) {
|
|
const { props } = this;
|
|
const isCollapsed = props.pref.collapsed;
|
|
const wasCollapsed = prevProps.pref.collapsed;
|
|
if (
|
|
// Don't send impression stats for the empty state
|
|
props.rows.length && (
|
|
// We only want to send impression stats if the content of the cards has changed
|
|
// and the section is not collapsed...
|
|
props.rows !== prevProps.rows && !isCollapsed ||
|
|
// or if we are expanding a section that was collapsed.
|
|
wasCollapsed && !isCollapsed)) {
|
|
this.sendImpressionStatsOrAddListener();
|
|
}
|
|
}
|
|
|
|
componentWillUpdate(nextProps) {
|
|
this.sendNewTabRehydrated(nextProps.initialized);
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
if (this._onVisibilityChange) {
|
|
this.props.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
|
|
}
|
|
}
|
|
|
|
needsImpressionStats(cards) {
|
|
if (!this.impressionCardGuids || this.impressionCardGuids.length !== cards.length) {
|
|
return true;
|
|
}
|
|
|
|
for (let i = 0; i < cards.length; i++) {
|
|
if (cards[i].guid !== this.impressionCardGuids[i]) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// The NEW_TAB_REHYDRATED event is used to inform feeds that their
|
|
// data has been consumed e.g. for counting the number of tabs that
|
|
// have rendered that data.
|
|
sendNewTabRehydrated(initialized) {
|
|
if (initialized && !this.renderNotified) {
|
|
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({ type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].NEW_TAB_REHYDRATED, data: {} }));
|
|
this.renderNotified = true;
|
|
}
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
id, eventSource, title, icon, rows, Pocket, topics,
|
|
emptyState, dispatch, compactCards, read_more_endpoint,
|
|
contextMenuOptions, initialized, learnMore,
|
|
pref, privacyNoticeURL, isFirst, isLast
|
|
} = this.props;
|
|
|
|
const waitingForSpoc = id === "topstories" && this.props.Pocket.waitingForSpoc;
|
|
const maxCardsPerRow = compactCards ? CARDS_PER_ROW_COMPACT_WIDE : CARDS_PER_ROW_DEFAULT;
|
|
const { numRows } = this;
|
|
const maxCards = maxCardsPerRow * numRows;
|
|
const maxCardsOnNarrow = CARDS_PER_ROW_DEFAULT * numRows;
|
|
|
|
const { pocketCta, isUserLoggedIn } = Pocket || {};
|
|
const { useCta } = pocketCta || {};
|
|
|
|
// Don't display anything until we have a definitve result from Pocket,
|
|
// to avoid a flash of logged out state while we render.
|
|
const isPocketLoggedInDefined = isUserLoggedIn === true || isUserLoggedIn === false;
|
|
|
|
const shouldShowPocketCta = id === "topstories" && useCta && isUserLoggedIn === false;
|
|
|
|
// Show topics only for top stories and if it has loaded with topics.
|
|
// The classs .top-stories-bottom-container ensures content doesn't shift as things load.
|
|
const shouldShowTopics = id === "topstories" && topics && topics.length > 0 && (useCta && isUserLoggedIn === true || !useCta && isPocketLoggedInDefined);
|
|
|
|
const realRows = rows.slice(0, maxCards);
|
|
|
|
// The empty state should only be shown after we have initialized and there is no content.
|
|
// Otherwise, we should show placeholders.
|
|
const shouldShowEmptyState = initialized && !rows.length;
|
|
|
|
const cards = [];
|
|
if (!shouldShowEmptyState) {
|
|
for (let i = 0; i < maxCards; i++) {
|
|
const link = realRows[i];
|
|
// On narrow viewports, we only show 3 cards per row. We'll mark the rest as
|
|
// .hide-for-narrow to hide in CSS via @media query.
|
|
const className = i >= maxCardsOnNarrow ? "hide-for-narrow" : "";
|
|
let usePlaceholder = !link;
|
|
// If we are in the third card and waiting for spoc,
|
|
// use the placeholder.
|
|
if (!usePlaceholder && i === 2 && waitingForSpoc) {
|
|
usePlaceholder = true;
|
|
}
|
|
cards.push(!usePlaceholder ? react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_Card_Card__WEBPACK_IMPORTED_MODULE_1__["Card"], { key: i,
|
|
index: i,
|
|
className: className,
|
|
dispatch: dispatch,
|
|
link: link,
|
|
contextMenuOptions: contextMenuOptions,
|
|
eventSource: eventSource,
|
|
shouldSendImpressionStats: this.props.shouldSendImpressionStats,
|
|
isWebExtension: this.props.isWebExtension }) : react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_Card_Card__WEBPACK_IMPORTED_MODULE_1__["PlaceholderCard"], { key: i, className: className }));
|
|
}
|
|
}
|
|
|
|
const sectionClassName = ["section", compactCards ? "compact-cards" : "normal-cards"].join(" ");
|
|
|
|
// <Section> <-- React component
|
|
// <section> <-- HTML5 element
|
|
return react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(
|
|
content_src_components_ComponentPerfTimer_ComponentPerfTimer__WEBPACK_IMPORTED_MODULE_4__["ComponentPerfTimer"],
|
|
this.props,
|
|
react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(
|
|
content_src_components_CollapsibleSection_CollapsibleSection__WEBPACK_IMPORTED_MODULE_3__["CollapsibleSection"],
|
|
{ className: sectionClassName, icon: icon,
|
|
title: title,
|
|
id: id,
|
|
eventSource: eventSource,
|
|
collapsed: this.props.pref.collapsed,
|
|
showPrefName: pref && pref.feed || id,
|
|
privacyNoticeURL: privacyNoticeURL,
|
|
Prefs: this.props.Prefs,
|
|
isFirst: isFirst,
|
|
isLast: isLast,
|
|
learnMore: learnMore,
|
|
dispatch: this.props.dispatch,
|
|
isWebExtension: this.props.isWebExtension },
|
|
!shouldShowEmptyState && react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(
|
|
"ul",
|
|
{ className: "section-list", style: { padding: 0 } },
|
|
cards
|
|
),
|
|
shouldShowEmptyState && react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(
|
|
"div",
|
|
{ className: "section-empty-state" },
|
|
react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(
|
|
"div",
|
|
{ className: "empty-state" },
|
|
emptyState.icon && emptyState.icon.startsWith("moz-extension://") ? react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement("img", { className: "empty-state-icon icon", style: { "background-image": `url('${emptyState.icon}')` } }) : react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement("img", { className: `empty-state-icon icon icon-${emptyState.icon}` }),
|
|
react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(
|
|
"p",
|
|
{ className: "empty-state-message" },
|
|
getFormattedMessage(emptyState.message)
|
|
)
|
|
)
|
|
),
|
|
id === "topstories" && react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(
|
|
"div",
|
|
{ className: "top-stories-bottom-container" },
|
|
react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(
|
|
"div",
|
|
null,
|
|
shouldShowTopics && react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_Topics_Topics__WEBPACK_IMPORTED_MODULE_9__["Topics"], { topics: this.props.topics }),
|
|
shouldShowPocketCta && react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_PocketLoggedInCta_PocketLoggedInCta__WEBPACK_IMPORTED_MODULE_7__["PocketLoggedInCta"], null)
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(
|
|
"div",
|
|
null,
|
|
read_more_endpoint && react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_MoreRecommendations_MoreRecommendations__WEBPACK_IMPORTED_MODULE_6__["MoreRecommendations"], { read_more_endpoint: read_more_endpoint })
|
|
)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
Section.defaultProps = {
|
|
document: global.document,
|
|
rows: [],
|
|
emptyState: {},
|
|
pref: {},
|
|
title: ""
|
|
};
|
|
|
|
const SectionIntl = Object(react_redux__WEBPACK_IMPORTED_MODULE_5__["connect"])(state => ({ Prefs: state.Prefs, Pocket: state.Pocket }))(Object(react_intl__WEBPACK_IMPORTED_MODULE_2__["injectIntl"])(Section));
|
|
|
|
class _Sections extends react__WEBPACK_IMPORTED_MODULE_8___default.a.PureComponent {
|
|
renderSections() {
|
|
const sections = [];
|
|
const enabledSections = this.props.Sections.filter(section => section.enabled);
|
|
const { sectionOrder, "feeds.topsites": showTopSites } = this.props.Prefs.values;
|
|
// Enabled sections doesn't include Top Sites, so we add it if enabled.
|
|
const expectedCount = enabledSections.length + ~~showTopSites;
|
|
|
|
for (const sectionId of sectionOrder.split(",")) {
|
|
const commonProps = {
|
|
key: sectionId,
|
|
isFirst: sections.length === 0,
|
|
isLast: sections.length === expectedCount - 1
|
|
};
|
|
if (sectionId === "topsites" && showTopSites) {
|
|
sections.push(react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_TopSites_TopSites__WEBPACK_IMPORTED_MODULE_10__["TopSites"], commonProps));
|
|
} else {
|
|
const section = enabledSections.find(s => s.id === sectionId);
|
|
if (section) {
|
|
sections.push(react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(SectionIntl, _extends({}, section, commonProps)));
|
|
}
|
|
}
|
|
}
|
|
return sections;
|
|
}
|
|
|
|
render() {
|
|
return react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(
|
|
"div",
|
|
{ className: "sections-list" },
|
|
this.renderSections()
|
|
);
|
|
}
|
|
}
|
|
|
|
const Sections = Object(react_redux__WEBPACK_IMPORTED_MODULE_5__["connect"])(state => ({ Sections: state.Sections, Prefs: state.Prefs }))(_Sections);
|
|
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
|
|
|
|
/***/ }),
|
|
/* 23 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "GetPlatformString", function() { return GetPlatformString; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "LinkMenuOptions", function() { return LinkMenuOptions; });
|
|
/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
|
|
|
|
|
|
const _OpenInPrivateWindow = site => ({
|
|
id: "menu_action_open_private_window",
|
|
icon: "new-window-private",
|
|
action: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({
|
|
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].OPEN_PRIVATE_WINDOW,
|
|
data: { url: site.url, referrer: site.referrer }
|
|
}),
|
|
userEvent: "OPEN_PRIVATE_WINDOW"
|
|
});
|
|
|
|
const GetPlatformString = platform => {
|
|
switch (platform) {
|
|
case "win":
|
|
return "menu_action_show_file_windows";
|
|
case "macosx":
|
|
return "menu_action_show_file_mac_os";
|
|
case "linux":
|
|
return "menu_action_show_file_linux";
|
|
default:
|
|
return "menu_action_show_file_default";
|
|
}
|
|
};
|
|
|
|
/**
|
|
* List of functions that return items that can be included as menu options in a
|
|
* LinkMenu. All functions take the site as the first parameter, and optionally
|
|
* the index of the site.
|
|
*/
|
|
const LinkMenuOptions = {
|
|
Separator: () => ({ type: "separator" }),
|
|
EmptyItem: () => ({ type: "empty" }),
|
|
RemoveBookmark: site => ({
|
|
id: "menu_action_remove_bookmark",
|
|
icon: "bookmark-added",
|
|
action: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({
|
|
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].DELETE_BOOKMARK_BY_ID,
|
|
data: site.bookmarkGuid
|
|
}),
|
|
userEvent: "BOOKMARK_DELETE"
|
|
}),
|
|
AddBookmark: site => ({
|
|
id: "menu_action_bookmark",
|
|
icon: "bookmark-hollow",
|
|
action: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({
|
|
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].BOOKMARK_URL,
|
|
data: { url: site.url, title: site.title, type: site.type }
|
|
}),
|
|
userEvent: "BOOKMARK_ADD"
|
|
}),
|
|
OpenInNewWindow: site => ({
|
|
id: "menu_action_open_new_window",
|
|
icon: "new-window",
|
|
action: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({
|
|
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].OPEN_NEW_WINDOW,
|
|
data: {
|
|
referrer: site.referrer,
|
|
typedBonus: site.typedBonus,
|
|
url: site.url
|
|
}
|
|
}),
|
|
userEvent: "OPEN_NEW_WINDOW"
|
|
}),
|
|
BlockUrl: (site, index, eventSource) => ({
|
|
id: "menu_action_dismiss",
|
|
icon: "dismiss",
|
|
action: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({
|
|
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].BLOCK_URL,
|
|
data: { url: site.open_url || site.url, pocket_id: site.pocket_id }
|
|
}),
|
|
impression: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].ImpressionStats({
|
|
source: eventSource,
|
|
block: 0,
|
|
tiles: [{ id: site.guid, pos: index }]
|
|
}),
|
|
userEvent: "BLOCK"
|
|
}),
|
|
|
|
// This is an option for web extentions which will result in remove items from
|
|
// memory and notify the web extenion, rather than using the built-in block list.
|
|
WebExtDismiss: (site, index, eventSource) => ({
|
|
id: "menu_action_webext_dismiss",
|
|
string_id: "menu_action_dismiss",
|
|
icon: "dismiss",
|
|
action: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].WebExtEvent(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].WEBEXT_DISMISS, {
|
|
source: eventSource,
|
|
url: site.url,
|
|
action_position: index
|
|
})
|
|
}),
|
|
DeleteUrl: (site, index, eventSource, isEnabled, siteInfo) => ({
|
|
id: "menu_action_delete",
|
|
icon: "delete",
|
|
action: {
|
|
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].DIALOG_OPEN,
|
|
data: {
|
|
onConfirm: [common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({ type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].DELETE_HISTORY_URL, data: { url: site.url, pocket_id: site.pocket_id, forceBlock: site.bookmarkGuid } }), common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent(Object.assign({ event: "DELETE", source: eventSource, action_position: index }, siteInfo))],
|
|
eventSource,
|
|
body_string_id: ["confirm_history_delete_p1", "confirm_history_delete_notice_p2"],
|
|
confirm_button_string_id: "menu_action_delete",
|
|
cancel_button_string_id: "topsites_form_cancel_button",
|
|
icon: "modal-delete"
|
|
}
|
|
},
|
|
userEvent: "DIALOG_OPEN"
|
|
}),
|
|
ShowFile: (site, index, eventSource, isEnabled, siteInfo, platform) => ({
|
|
id: GetPlatformString(platform),
|
|
icon: "search",
|
|
action: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({
|
|
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].SHOW_DOWNLOAD_FILE,
|
|
data: { url: site.url }
|
|
})
|
|
}),
|
|
OpenFile: site => ({
|
|
id: "menu_action_open_file",
|
|
icon: "open-file",
|
|
action: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({
|
|
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].OPEN_DOWNLOAD_FILE,
|
|
data: { url: site.url }
|
|
})
|
|
}),
|
|
CopyDownloadLink: site => ({
|
|
id: "menu_action_copy_download_link",
|
|
icon: "copy",
|
|
action: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({
|
|
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].COPY_DOWNLOAD_LINK,
|
|
data: { url: site.url }
|
|
})
|
|
}),
|
|
GoToDownloadPage: site => ({
|
|
id: "menu_action_go_to_download_page",
|
|
icon: "download",
|
|
action: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({
|
|
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].OPEN_LINK,
|
|
data: { url: site.referrer }
|
|
}),
|
|
disabled: !site.referrer
|
|
}),
|
|
RemoveDownload: site => ({
|
|
id: "menu_action_remove_download",
|
|
icon: "delete",
|
|
action: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({
|
|
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].REMOVE_DOWNLOAD_FILE,
|
|
data: { url: site.url }
|
|
})
|
|
}),
|
|
PinTopSite: ({ url, searchTopSite, label }, index) => ({
|
|
id: "menu_action_pin",
|
|
icon: "pin",
|
|
action: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({
|
|
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].TOP_SITES_PIN,
|
|
data: {
|
|
site: Object.assign({
|
|
url
|
|
}, searchTopSite && { searchTopSite, label }),
|
|
index
|
|
}
|
|
}),
|
|
userEvent: "PIN"
|
|
}),
|
|
UnpinTopSite: site => ({
|
|
id: "menu_action_unpin",
|
|
icon: "unpin",
|
|
action: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({
|
|
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].TOP_SITES_UNPIN,
|
|
data: { site: { url: site.url } }
|
|
}),
|
|
userEvent: "UNPIN"
|
|
}),
|
|
SaveToPocket: (site, index, eventSource) => ({
|
|
id: "menu_action_save_to_pocket",
|
|
icon: "pocket-save",
|
|
action: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({
|
|
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].SAVE_TO_POCKET,
|
|
data: { site: { url: site.url, title: site.title } }
|
|
}),
|
|
impression: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].ImpressionStats({
|
|
source: eventSource,
|
|
pocket: 0,
|
|
tiles: [{ id: site.guid, pos: index }]
|
|
}),
|
|
userEvent: "SAVE_TO_POCKET"
|
|
}),
|
|
DeleteFromPocket: site => ({
|
|
id: "menu_action_delete_pocket",
|
|
icon: "delete",
|
|
action: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({
|
|
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].DELETE_FROM_POCKET,
|
|
data: { pocket_id: site.pocket_id }
|
|
}),
|
|
userEvent: "DELETE_FROM_POCKET"
|
|
}),
|
|
ArchiveFromPocket: site => ({
|
|
id: "menu_action_archive_pocket",
|
|
icon: "check",
|
|
action: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({
|
|
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].ARCHIVE_FROM_POCKET,
|
|
data: { pocket_id: site.pocket_id }
|
|
}),
|
|
userEvent: "ARCHIVE_FROM_POCKET"
|
|
}),
|
|
EditTopSite: (site, index) => ({
|
|
id: "edit_topsites_button_text",
|
|
icon: "edit",
|
|
action: {
|
|
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].TOP_SITES_EDIT,
|
|
data: { index }
|
|
}
|
|
}),
|
|
CheckBookmark: site => site.bookmarkGuid ? LinkMenuOptions.RemoveBookmark(site) : LinkMenuOptions.AddBookmark(site),
|
|
CheckPinTopSite: (site, index) => site.isPinned ? LinkMenuOptions.UnpinTopSite(site) : LinkMenuOptions.PinTopSite(site, index),
|
|
CheckSavedToPocket: (site, index) => site.pocket_id ? LinkMenuOptions.DeleteFromPocket(site) : LinkMenuOptions.SaveToPocket(site, index),
|
|
CheckBookmarkOrArchive: site => site.pocket_id ? LinkMenuOptions.ArchiveFromPocket(site) : LinkMenuOptions.CheckBookmark(site),
|
|
OpenInPrivateWindow: (site, index, eventSource, isEnabled) => isEnabled ? _OpenInPrivateWindow(site) : LinkMenuOptions.EmptyItem()
|
|
};
|
|
|
|
/***/ }),
|
|
/* 24 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_LinkMenu", function() { return _LinkMenu; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "LinkMenu", function() { return LinkMenu; });
|
|
/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
|
|
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(16);
|
|
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_1__);
|
|
/* harmony import */ var content_src_components_ContextMenu_ContextMenu__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(25);
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(13);
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_3__);
|
|
/* harmony import */ var content_src_lib_link_menu_options__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(23);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(9);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_5__);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const DEFAULT_SITE_MENU_OPTIONS = ["CheckPinTopSite", "EditTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"];
|
|
|
|
class _LinkMenu extends react__WEBPACK_IMPORTED_MODULE_5___default.a.PureComponent {
|
|
getOptions() {
|
|
const { props } = this;
|
|
const { site, index, source, isPrivateBrowsingEnabled, siteInfo, platform } = props;
|
|
|
|
// Handle special case of default site
|
|
const propOptions = !site.isDefault || site.searchTopSite ? props.options : DEFAULT_SITE_MENU_OPTIONS;
|
|
|
|
const options = propOptions.map(o => content_src_lib_link_menu_options__WEBPACK_IMPORTED_MODULE_4__["LinkMenuOptions"][o](site, index, source, isPrivateBrowsingEnabled, siteInfo, platform)).map(option => {
|
|
const { action, impression, id, string_id, type, userEvent } = option;
|
|
if (!type && id) {
|
|
option.label = props.intl.formatMessage({ id: string_id || id });
|
|
option.onClick = () => {
|
|
props.dispatch(action);
|
|
if (userEvent) {
|
|
const userEventData = Object.assign({
|
|
event: userEvent,
|
|
source,
|
|
action_position: index
|
|
}, siteInfo);
|
|
props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent(userEventData));
|
|
}
|
|
if (impression && props.shouldSendImpressionStats) {
|
|
props.dispatch(impression);
|
|
}
|
|
};
|
|
}
|
|
return option;
|
|
});
|
|
|
|
// This is for accessibility to support making each item tabbable.
|
|
// We want to know which item is the first and which item
|
|
// is the last, so we can close the context menu accordingly.
|
|
options[0].first = true;
|
|
options[options.length - 1].last = true;
|
|
return options;
|
|
}
|
|
|
|
render() {
|
|
return react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(content_src_components_ContextMenu_ContextMenu__WEBPACK_IMPORTED_MODULE_2__["ContextMenu"], {
|
|
onUpdate: this.props.onUpdate,
|
|
options: this.getOptions() });
|
|
}
|
|
}
|
|
|
|
const getState = state => ({ isPrivateBrowsingEnabled: state.Prefs.values.isPrivateBrowsingEnabled, platform: state.Prefs.values.platform });
|
|
const LinkMenu = Object(react_redux__WEBPACK_IMPORTED_MODULE_1__["connect"])(getState)(Object(react_intl__WEBPACK_IMPORTED_MODULE_3__["injectIntl"])(_LinkMenu));
|
|
|
|
/***/ }),
|
|
/* 25 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ContextMenu", function() { return ContextMenu; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ContextMenuItem", function() { return ContextMenuItem; });
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(9);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
|
|
|
|
|
|
class ContextMenu extends react__WEBPACK_IMPORTED_MODULE_0___default.a.PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
this.hideContext = this.hideContext.bind(this);
|
|
this.onClick = this.onClick.bind(this);
|
|
}
|
|
|
|
hideContext() {
|
|
this.props.onUpdate(false);
|
|
}
|
|
|
|
componentDidMount() {
|
|
setTimeout(() => {
|
|
global.addEventListener("click", this.hideContext);
|
|
}, 0);
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
global.removeEventListener("click", this.hideContext);
|
|
}
|
|
|
|
onClick(event) {
|
|
// Eat all clicks on the context menu so they don't bubble up to window.
|
|
// This prevents the context menu from closing when clicking disabled items
|
|
// or the separators.
|
|
event.stopPropagation();
|
|
}
|
|
|
|
render() {
|
|
return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(
|
|
"span",
|
|
{ className: "context-menu", onClick: this.onClick },
|
|
react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(
|
|
"ul",
|
|
{ role: "menu", className: "context-menu-list" },
|
|
this.props.options.map((option, i) => option.type === "separator" ? react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("li", { key: i, className: "separator" }) : option.type !== "empty" && react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(ContextMenuItem, { key: i, option: option, hideContext: this.hideContext }))
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
class ContextMenuItem extends react__WEBPACK_IMPORTED_MODULE_0___default.a.PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
this.onClick = this.onClick.bind(this);
|
|
this.onKeyDown = this.onKeyDown.bind(this);
|
|
}
|
|
|
|
onClick() {
|
|
this.props.hideContext();
|
|
this.props.option.onClick();
|
|
}
|
|
|
|
onKeyDown(event) {
|
|
const { option } = this.props;
|
|
switch (event.key) {
|
|
case "Tab":
|
|
// tab goes down in context menu, shift + tab goes up in context menu
|
|
// if we're on the last item, one more tab will close the context menu
|
|
// similarly, if we're on the first item, one more shift + tab will close it
|
|
if (event.shiftKey && option.first || !event.shiftKey && option.last) {
|
|
this.props.hideContext();
|
|
}
|
|
break;
|
|
case "Enter":
|
|
this.props.hideContext();
|
|
option.onClick();
|
|
break;
|
|
}
|
|
}
|
|
|
|
render() {
|
|
const { option } = this.props;
|
|
return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(
|
|
"li",
|
|
{ role: "menuitem", className: "context-menu-item" },
|
|
react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(
|
|
"a",
|
|
{ onClick: this.onClick, onKeyDown: this.onKeyDown, tabIndex: "0", className: option.disabled ? "disabled" : "" },
|
|
option.icon && react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("span", { className: `icon icon-spacer icon-${option.icon}` }),
|
|
option.label
|
|
)
|
|
);
|
|
}
|
|
}
|
|
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
|
|
|
|
/***/ }),
|
|
/* 26 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ScreenshotUtils", function() { return ScreenshotUtils; });
|
|
/**
|
|
* List of helper functions for screenshot-based images.
|
|
*
|
|
* There are two kinds of images:
|
|
* 1. Remote Image: This is the image from the main process and it refers to
|
|
* the image in the React props. This can either be an object with the `data`
|
|
* and `path` properties, if it is a blob, or a string, if it is a normal image.
|
|
* 2. Local Image: This is the image object in the content process and it refers
|
|
* to the image *object* in the React component's state. All local image
|
|
* objects have the `url` property, and an additional property `path`, if they
|
|
* are blobs.
|
|
*/
|
|
const ScreenshotUtils = {
|
|
isBlob(isLocal, image) {
|
|
return !!(image && image.path && (!isLocal && image.data || isLocal && image.url));
|
|
},
|
|
|
|
// This should always be called with a remote image and not a local image.
|
|
createLocalImageObject(remoteImage) {
|
|
if (!remoteImage) {
|
|
return null;
|
|
}
|
|
if (this.isBlob(false, remoteImage)) {
|
|
return { url: global.URL.createObjectURL(remoteImage.data), path: remoteImage.path };
|
|
}
|
|
return { url: remoteImage };
|
|
},
|
|
|
|
// Revokes the object URL of the image if the local image is a blob.
|
|
// This should always be called with a local image and not a remote image.
|
|
maybeRevokeBlobObjectURL(localImage) {
|
|
if (this.isBlob(true, localImage)) {
|
|
global.URL.revokeObjectURL(localImage.url);
|
|
}
|
|
},
|
|
|
|
// Checks if remoteImage and localImage are the same.
|
|
isRemoteImageLocal(localImage, remoteImage) {
|
|
// Both remoteImage and localImage are present.
|
|
if (remoteImage && localImage) {
|
|
return this.isBlob(false, remoteImage) ? localImage.path === remoteImage.path : localImage.url === remoteImage;
|
|
}
|
|
|
|
// This will only handle the remaining three possible outcomes.
|
|
// (i.e. everything except when both image and localImage are present)
|
|
return !remoteImage && !localImage;
|
|
}
|
|
};
|
|
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
|
|
|
|
/***/ }),
|
|
/* 27 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_CollapsibleSection", function() { return _CollapsibleSection; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "CollapsibleSection", function() { return CollapsibleSection; });
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(13);
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_0__);
|
|
/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2);
|
|
/* harmony import */ var content_src_components_ErrorBoundary_ErrorBoundary__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(17);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(9);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_3__);
|
|
/* harmony import */ var content_src_components_SectionMenu_SectionMenu__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(28);
|
|
/* harmony import */ var content_src_lib_section_menu_options__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(29);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const VISIBLE = "visible";
|
|
const VISIBILITY_CHANGE_EVENT = "visibilitychange";
|
|
|
|
function getFormattedMessage(message) {
|
|
return typeof message === "string" ? react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"span",
|
|
null,
|
|
message
|
|
) : react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_0__["FormattedMessage"], message);
|
|
}
|
|
|
|
class _CollapsibleSection extends react__WEBPACK_IMPORTED_MODULE_3___default.a.PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
this.onBodyMount = this.onBodyMount.bind(this);
|
|
this.onHeaderClick = this.onHeaderClick.bind(this);
|
|
this.onTransitionEnd = this.onTransitionEnd.bind(this);
|
|
this.enableOrDisableAnimation = this.enableOrDisableAnimation.bind(this);
|
|
this.onMenuButtonClick = this.onMenuButtonClick.bind(this);
|
|
this.onMenuButtonMouseEnter = this.onMenuButtonMouseEnter.bind(this);
|
|
this.onMenuButtonMouseLeave = this.onMenuButtonMouseLeave.bind(this);
|
|
this.onMenuUpdate = this.onMenuUpdate.bind(this);
|
|
this.state = { enableAnimation: true, isAnimating: false, menuButtonHover: false, showContextMenu: false };
|
|
}
|
|
|
|
componentWillMount() {
|
|
this.props.document.addEventListener(VISIBILITY_CHANGE_EVENT, this.enableOrDisableAnimation);
|
|
}
|
|
|
|
componentWillUpdate(nextProps) {
|
|
// Check if we're about to go from expanded to collapsed
|
|
if (!this.props.collapsed && nextProps.collapsed) {
|
|
// This next line forces a layout flush of the section body, which has a
|
|
// max-height style set, so that the upcoming collapse animation can
|
|
// animate from that height to the collapsed height. Without this, the
|
|
// update is coalesced and there's no animation from no-max-height to 0.
|
|
this.sectionBody.scrollHeight; // eslint-disable-line no-unused-expressions
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
this.props.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this.enableOrDisableAnimation);
|
|
}
|
|
|
|
enableOrDisableAnimation() {
|
|
// Only animate the collapse/expand for visible tabs.
|
|
const visible = this.props.document.visibilityState === VISIBLE;
|
|
if (this.state.enableAnimation !== visible) {
|
|
this.setState({ enableAnimation: visible });
|
|
}
|
|
}
|
|
|
|
onBodyMount(node) {
|
|
this.sectionBody = node;
|
|
}
|
|
|
|
onHeaderClick() {
|
|
// If this.sectionBody is unset, it means that we're in some sort of error
|
|
// state, probably displaying the error fallback, so we won't be able to
|
|
// compute the height, and we don't want to persist the preference.
|
|
// If props.collapsed is undefined handler shouldn't do anything.
|
|
if (!this.sectionBody || this.props.collapsed === undefined) {
|
|
return;
|
|
}
|
|
|
|
// Get the current height of the body so max-height transitions can work
|
|
this.setState({
|
|
isAnimating: true,
|
|
maxHeight: `${this._getSectionBodyHeight()}px`
|
|
});
|
|
const { action, userEvent } = content_src_lib_section_menu_options__WEBPACK_IMPORTED_MODULE_5__["SectionMenuOptions"].CheckCollapsed(this.props);
|
|
this.props.dispatch(action);
|
|
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_1__["actionCreators"].UserEvent({
|
|
event: userEvent,
|
|
source: this.props.source
|
|
}));
|
|
}
|
|
|
|
_getSectionBodyHeight() {
|
|
const div = this.sectionBody;
|
|
if (div.style.display === "none") {
|
|
// If the div isn't displayed, we can't get it's height. So we display it
|
|
// to get the height (it doesn't show up because max-height is set to 0px
|
|
// in CSS). We don't undo this because we are about to expand the section.
|
|
div.style.display = "block";
|
|
}
|
|
return div.scrollHeight;
|
|
}
|
|
|
|
onTransitionEnd(event) {
|
|
// Only update the animating state for our own transition (not a child's)
|
|
if (event.target === event.currentTarget) {
|
|
this.setState({ isAnimating: false });
|
|
}
|
|
}
|
|
|
|
renderIcon() {
|
|
const { icon } = this.props;
|
|
if (icon && icon.startsWith("moz-extension://")) {
|
|
return react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("span", { className: "icon icon-small-spacer", style: { backgroundImage: `url('${icon}')` } });
|
|
}
|
|
return react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("span", { className: `icon icon-small-spacer icon-${icon || "webextension"}` });
|
|
}
|
|
|
|
onMenuButtonClick(event) {
|
|
event.preventDefault();
|
|
this.setState({ showContextMenu: true });
|
|
}
|
|
|
|
onMenuButtonMouseEnter() {
|
|
this.setState({ menuButtonHover: true });
|
|
}
|
|
|
|
onMenuButtonMouseLeave() {
|
|
this.setState({ menuButtonHover: false });
|
|
}
|
|
|
|
onMenuUpdate(showContextMenu) {
|
|
this.setState({ showContextMenu });
|
|
}
|
|
|
|
render() {
|
|
const isCollapsible = this.props.collapsed !== undefined;
|
|
const { enableAnimation, isAnimating, maxHeight, menuButtonHover, showContextMenu } = this.state;
|
|
const { id, eventSource, collapsed, learnMore, title, extraMenuOptions, showPrefName, privacyNoticeURL, dispatch, isFirst, isLast, isWebExtension } = this.props;
|
|
const active = menuButtonHover || showContextMenu;
|
|
let bodyStyle;
|
|
if (isAnimating && !collapsed) {
|
|
bodyStyle = { maxHeight };
|
|
} else if (!isAnimating && collapsed) {
|
|
bodyStyle = { display: "none" };
|
|
}
|
|
return react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"section",
|
|
{
|
|
className: `collapsible-section ${this.props.className}${enableAnimation ? " animation-enabled" : ""}${collapsed ? " collapsed" : ""}${active ? " active" : ""}`
|
|
// Note: data-section-id is used for web extension api tests in mozilla central
|
|
, "data-section-id": id },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"div",
|
|
{ className: "section-top-bar" },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"h3",
|
|
{ className: "section-title" },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"span",
|
|
{ className: "click-target-container" },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"span",
|
|
{ className: "click-target", onClick: this.onHeaderClick },
|
|
this.renderIcon(),
|
|
getFormattedMessage(title)
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"span",
|
|
{ className: "click-target", onClick: this.onHeaderClick },
|
|
isCollapsible && react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("span", { className: `collapsible-arrow icon ${collapsed ? "icon-arrowhead-forward-small" : "icon-arrowhead-down-small"}` })
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"span",
|
|
null,
|
|
learnMore && react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"span",
|
|
{ className: "learn-more-link" },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"a",
|
|
{ href: learnMore.link.href },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_0__["FormattedMessage"], { id: learnMore.link.id })
|
|
)
|
|
)
|
|
)
|
|
)
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"div",
|
|
null,
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"button",
|
|
{
|
|
className: "context-menu-button icon",
|
|
title: this.props.intl.formatMessage({ id: "context_menu_title" }),
|
|
onClick: this.onMenuButtonClick,
|
|
onMouseEnter: this.onMenuButtonMouseEnter,
|
|
onMouseLeave: this.onMenuButtonMouseLeave },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"span",
|
|
{ className: "sr-only" },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_0__["FormattedMessage"], { id: "section_context_menu_button_sr" })
|
|
)
|
|
),
|
|
showContextMenu && react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(content_src_components_SectionMenu_SectionMenu__WEBPACK_IMPORTED_MODULE_4__["SectionMenu"], {
|
|
id: id,
|
|
extraOptions: extraMenuOptions,
|
|
eventSource: eventSource,
|
|
showPrefName: showPrefName,
|
|
privacyNoticeURL: privacyNoticeURL,
|
|
collapsed: collapsed,
|
|
onUpdate: this.onMenuUpdate,
|
|
isFirst: isFirst,
|
|
isLast: isLast,
|
|
dispatch: dispatch,
|
|
isWebExtension: isWebExtension })
|
|
)
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
content_src_components_ErrorBoundary_ErrorBoundary__WEBPACK_IMPORTED_MODULE_2__["ErrorBoundary"],
|
|
{ className: "section-body-fallback" },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"div",
|
|
{
|
|
className: `section-body${isAnimating ? " animating" : ""}`,
|
|
onTransitionEnd: this.onTransitionEnd,
|
|
ref: this.onBodyMount,
|
|
style: bodyStyle },
|
|
this.props.children
|
|
)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
_CollapsibleSection.defaultProps = {
|
|
document: global.document || {
|
|
addEventListener: () => {},
|
|
removeEventListener: () => {},
|
|
visibilityState: "hidden"
|
|
},
|
|
Prefs: { values: {} }
|
|
};
|
|
|
|
const CollapsibleSection = Object(react_intl__WEBPACK_IMPORTED_MODULE_0__["injectIntl"])(_CollapsibleSection);
|
|
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
|
|
|
|
/***/ }),
|
|
/* 28 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_SectionMenu", function() { return _SectionMenu; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SectionMenu", function() { return SectionMenu; });
|
|
/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
|
|
/* harmony import */ var content_src_components_ContextMenu_ContextMenu__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(25);
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(13);
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_2__);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(9);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_3__);
|
|
/* harmony import */ var content_src_lib_section_menu_options__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(29);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const DEFAULT_SECTION_MENU_OPTIONS = ["MoveUp", "MoveDown", "Separator", "RemoveSection", "CheckCollapsed", "Separator", "ManageSection"];
|
|
const WEBEXT_SECTION_MENU_OPTIONS = ["MoveUp", "MoveDown", "Separator", "CheckCollapsed", "Separator", "ManageWebExtension"];
|
|
|
|
class _SectionMenu extends react__WEBPACK_IMPORTED_MODULE_3___default.a.PureComponent {
|
|
getOptions() {
|
|
const { props } = this;
|
|
|
|
const propOptions = props.isWebExtension ? [...WEBEXT_SECTION_MENU_OPTIONS] : [...DEFAULT_SECTION_MENU_OPTIONS];
|
|
// Prepend custom options and a separator
|
|
if (props.extraOptions) {
|
|
propOptions.splice(0, 0, ...props.extraOptions, "Separator");
|
|
}
|
|
// Insert privacy notice before the last option ("ManageSection")
|
|
if (props.privacyNoticeURL) {
|
|
propOptions.splice(-1, 0, "PrivacyNotice");
|
|
}
|
|
|
|
const options = propOptions.map(o => content_src_lib_section_menu_options__WEBPACK_IMPORTED_MODULE_4__["SectionMenuOptions"][o](props)).map(option => {
|
|
const { action, id, type, userEvent } = option;
|
|
if (!type && id) {
|
|
option.label = props.intl.formatMessage({ id });
|
|
option.onClick = () => {
|
|
props.dispatch(action);
|
|
if (userEvent) {
|
|
props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent({
|
|
event: userEvent,
|
|
source: props.source
|
|
}));
|
|
}
|
|
};
|
|
}
|
|
return option;
|
|
});
|
|
|
|
// This is for accessibility to support making each item tabbable.
|
|
// We want to know which item is the first and which item
|
|
// is the last, so we can close the context menu accordingly.
|
|
options[0].first = true;
|
|
options[options.length - 1].last = true;
|
|
return options;
|
|
}
|
|
|
|
render() {
|
|
return react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(content_src_components_ContextMenu_ContextMenu__WEBPACK_IMPORTED_MODULE_1__["ContextMenu"], {
|
|
onUpdate: this.props.onUpdate,
|
|
options: this.getOptions() });
|
|
}
|
|
}
|
|
|
|
const SectionMenu = Object(react_intl__WEBPACK_IMPORTED_MODULE_2__["injectIntl"])(_SectionMenu);
|
|
|
|
/***/ }),
|
|
/* 29 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SectionMenuOptions", function() { return SectionMenuOptions; });
|
|
/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
|
|
|
|
|
|
/**
|
|
* List of functions that return items that can be included as menu options in a
|
|
* SectionMenu. All functions take the section as the only parameter.
|
|
*/
|
|
const SectionMenuOptions = {
|
|
Separator: () => ({ type: "separator" }),
|
|
MoveUp: section => ({
|
|
id: "section_menu_action_move_up",
|
|
icon: "arrowhead-up",
|
|
action: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({
|
|
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].SECTION_MOVE,
|
|
data: { id: section.id, direction: -1 }
|
|
}),
|
|
userEvent: "MENU_MOVE_UP",
|
|
disabled: !!section.isFirst
|
|
}),
|
|
MoveDown: section => ({
|
|
id: "section_menu_action_move_down",
|
|
icon: "arrowhead-down",
|
|
action: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({
|
|
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].SECTION_MOVE,
|
|
data: { id: section.id, direction: +1 }
|
|
}),
|
|
userEvent: "MENU_MOVE_DOWN",
|
|
disabled: !!section.isLast
|
|
}),
|
|
RemoveSection: section => ({
|
|
id: "section_menu_action_remove_section",
|
|
icon: "dismiss",
|
|
action: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].SetPref(section.showPrefName, false),
|
|
userEvent: "MENU_REMOVE"
|
|
}),
|
|
CollapseSection: section => ({
|
|
id: "section_menu_action_collapse_section",
|
|
icon: "minimize",
|
|
action: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({ type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].UPDATE_SECTION_PREFS, data: { id: section.id, value: { collapsed: true } } }),
|
|
userEvent: "MENU_COLLAPSE"
|
|
}),
|
|
ExpandSection: section => ({
|
|
id: "section_menu_action_expand_section",
|
|
icon: "maximize",
|
|
action: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({ type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].UPDATE_SECTION_PREFS, data: { id: section.id, value: { collapsed: false } } }),
|
|
userEvent: "MENU_EXPAND"
|
|
}),
|
|
ManageSection: section => ({
|
|
id: "section_menu_action_manage_section",
|
|
icon: "settings",
|
|
action: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({ type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].SETTINGS_OPEN }),
|
|
userEvent: "MENU_MANAGE"
|
|
}),
|
|
ManageWebExtension: section => ({
|
|
id: "section_menu_action_manage_webext",
|
|
icon: "settings",
|
|
action: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({ type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].OPEN_WEBEXT_SETTINGS, data: section.id })
|
|
}),
|
|
AddTopSite: section => ({
|
|
id: "section_menu_action_add_topsite",
|
|
icon: "add",
|
|
action: { type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].TOP_SITES_EDIT, data: { index: -1 } },
|
|
userEvent: "MENU_ADD_TOPSITE"
|
|
}),
|
|
AddSearchShortcut: section => ({
|
|
id: "section_menu_action_add_search_engine",
|
|
icon: "search",
|
|
action: { type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL },
|
|
userEvent: "MENU_ADD_SEARCH"
|
|
}),
|
|
PrivacyNotice: section => ({
|
|
id: "section_menu_action_privacy_notice",
|
|
icon: "info",
|
|
action: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({
|
|
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].OPEN_LINK,
|
|
data: { url: section.privacyNoticeURL }
|
|
}),
|
|
userEvent: "MENU_PRIVACY_NOTICE"
|
|
}),
|
|
CheckCollapsed: section => section.collapsed ? SectionMenuOptions.ExpandSection(section) : SectionMenuOptions.CollapseSection(section)
|
|
};
|
|
|
|
/***/ }),
|
|
/* 30 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ComponentPerfTimer", function() { return ComponentPerfTimer; });
|
|
/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
|
|
/* harmony import */ var common_PerfService_jsm__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(31);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(9);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_2__);
|
|
|
|
|
|
|
|
|
|
// Currently record only a fixed set of sections. This will prevent data
|
|
// from custom sections from showing up or from topstories.
|
|
const RECORDED_SECTIONS = ["highlights", "topsites"];
|
|
|
|
class ComponentPerfTimer extends react__WEBPACK_IMPORTED_MODULE_2___default.a.Component {
|
|
constructor(props) {
|
|
super(props);
|
|
// Just for test dependency injection:
|
|
this.perfSvc = this.props.perfSvc || common_PerfService_jsm__WEBPACK_IMPORTED_MODULE_1__["perfService"];
|
|
|
|
this._sendBadStateEvent = this._sendBadStateEvent.bind(this);
|
|
this._sendPaintedEvent = this._sendPaintedEvent.bind(this);
|
|
this._reportMissingData = false;
|
|
this._timestampHandled = false;
|
|
this._recordedFirstRender = false;
|
|
}
|
|
|
|
componentDidMount() {
|
|
if (!RECORDED_SECTIONS.includes(this.props.id)) {
|
|
return;
|
|
}
|
|
|
|
this._maybeSendPaintedEvent();
|
|
}
|
|
|
|
componentDidUpdate() {
|
|
if (!RECORDED_SECTIONS.includes(this.props.id)) {
|
|
return;
|
|
}
|
|
|
|
this._maybeSendPaintedEvent();
|
|
}
|
|
|
|
/**
|
|
* Call the given callback after the upcoming frame paints.
|
|
*
|
|
* @note Both setTimeout and requestAnimationFrame are throttled when the page
|
|
* is hidden, so this callback may get called up to a second or so after the
|
|
* requestAnimationFrame "paint" for hidden tabs.
|
|
*
|
|
* Newtabs hidden while loading will presumably be fairly rare (other than
|
|
* preloaded tabs, which we will be filtering out on the server side), so such
|
|
* cases should get lost in the noise.
|
|
*
|
|
* If we decide that it's important to find out when something that's hidden
|
|
* has "painted", however, another option is to post a message to this window.
|
|
* That should happen even faster than setTimeout, and, at least as of this
|
|
* writing, it's not throttled in hidden windows in Firefox.
|
|
*
|
|
* @param {Function} callback
|
|
*
|
|
* @returns void
|
|
*/
|
|
_afterFramePaint(callback) {
|
|
requestAnimationFrame(() => setTimeout(callback, 0));
|
|
}
|
|
|
|
_maybeSendBadStateEvent() {
|
|
// Follow up bugs:
|
|
// https://github.com/mozilla/activity-stream/issues/3691
|
|
if (!this.props.initialized) {
|
|
// Remember to report back when data is available.
|
|
this._reportMissingData = true;
|
|
} else if (this._reportMissingData) {
|
|
this._reportMissingData = false;
|
|
// Report how long it took for component to become initialized.
|
|
this._sendBadStateEvent();
|
|
}
|
|
}
|
|
|
|
_maybeSendPaintedEvent() {
|
|
// If we've already handled a timestamp, don't do it again.
|
|
if (this._timestampHandled || !this.props.initialized) {
|
|
return;
|
|
}
|
|
|
|
// And if we haven't, we're doing so now, so remember that. Even if
|
|
// something goes wrong in the callback, we can't try again, as we'd be
|
|
// sending back the wrong data, and we have to do it here, so that other
|
|
// calls to this method while waiting for the next frame won't also try to
|
|
// handle it.
|
|
this._timestampHandled = true;
|
|
this._afterFramePaint(this._sendPaintedEvent);
|
|
}
|
|
|
|
/**
|
|
* Triggered by call to render. Only first call goes through due to
|
|
* `_recordedFirstRender`.
|
|
*/
|
|
_ensureFirstRenderTsRecorded() {
|
|
// Used as t0 for recording how long component took to initialize.
|
|
if (!this._recordedFirstRender) {
|
|
this._recordedFirstRender = true;
|
|
// topsites_first_render_ts, highlights_first_render_ts.
|
|
const key = `${this.props.id}_first_render_ts`;
|
|
this.perfSvc.mark(key);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates `TELEMETRY_UNDESIRED_EVENT` with timestamp in ms
|
|
* of how much longer the data took to be ready for display than it would
|
|
* have been the ideal case.
|
|
* https://github.com/mozilla/ping-centre/issues/98
|
|
*/
|
|
_sendBadStateEvent() {
|
|
// highlights_data_ready_ts, topsites_data_ready_ts.
|
|
const dataReadyKey = `${this.props.id}_data_ready_ts`;
|
|
this.perfSvc.mark(dataReadyKey);
|
|
|
|
try {
|
|
const firstRenderKey = `${this.props.id}_first_render_ts`;
|
|
// value has to be Int32.
|
|
const value = parseInt(this.perfSvc.getMostRecentAbsMarkStartByName(dataReadyKey) - this.perfSvc.getMostRecentAbsMarkStartByName(firstRenderKey), 10);
|
|
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({
|
|
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].SAVE_SESSION_PERF_DATA,
|
|
// highlights_data_late_by_ms, topsites_data_late_by_ms.
|
|
data: { [`${this.props.id}_data_late_by_ms`]: value }
|
|
}));
|
|
} catch (ex) {
|
|
// If this failed, it's likely because the `privacy.resistFingerprinting`
|
|
// pref is true.
|
|
}
|
|
}
|
|
|
|
_sendPaintedEvent() {
|
|
// Record first_painted event but only send if topsites.
|
|
if (this.props.id !== "topsites") {
|
|
return;
|
|
}
|
|
|
|
// topsites_first_painted_ts.
|
|
const key = `${this.props.id}_first_painted_ts`;
|
|
this.perfSvc.mark(key);
|
|
|
|
try {
|
|
const data = {};
|
|
data[key] = this.perfSvc.getMostRecentAbsMarkStartByName(key);
|
|
|
|
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({
|
|
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].SAVE_SESSION_PERF_DATA,
|
|
data
|
|
}));
|
|
} catch (ex) {
|
|
// If this failed, it's likely because the `privacy.resistFingerprinting`
|
|
// pref is true. We should at least not blow up, and should continue
|
|
// to set this._timestampHandled to avoid going through this again.
|
|
}
|
|
}
|
|
|
|
render() {
|
|
if (RECORDED_SECTIONS.includes(this.props.id)) {
|
|
this._ensureFirstRenderTsRecorded();
|
|
this._maybeSendBadStateEvent();
|
|
}
|
|
return this.props.children;
|
|
}
|
|
}
|
|
|
|
/***/ }),
|
|
/* 31 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_PerfService", function() { return _PerfService; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "perfService", function() { return perfService; });
|
|
/* globals Services */
|
|
|
|
|
|
/* istanbul ignore if */
|
|
|
|
if (typeof ChromeUtils !== "undefined") {
|
|
ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
}
|
|
|
|
let usablePerfObj;
|
|
|
|
/* istanbul ignore if */
|
|
/* istanbul ignore else */
|
|
if (typeof Services !== "undefined") {
|
|
// Borrow the high-resolution timer from the hidden window....
|
|
usablePerfObj = Services.appShell.hiddenDOMWindow.performance;
|
|
} else if (typeof performance !== "undefined") {
|
|
// we must be running in content space
|
|
// eslint-disable-next-line no-undef
|
|
usablePerfObj = performance;
|
|
} else {
|
|
// This is a dummy object so this file doesn't crash in the node prerendering
|
|
// task.
|
|
usablePerfObj = {
|
|
now() {},
|
|
mark() {}
|
|
};
|
|
}
|
|
|
|
function _PerfService(options) {
|
|
// For testing, so that we can use a fake Window.performance object with
|
|
// known state.
|
|
if (options && options.performanceObj) {
|
|
this._perf = options.performanceObj;
|
|
} else {
|
|
this._perf = usablePerfObj;
|
|
}
|
|
}
|
|
|
|
|
|
_PerfService.prototype = {
|
|
/**
|
|
* Calls the underlying mark() method on the appropriate Window.performance
|
|
* object to add a mark with the given name to the appropriate performance
|
|
* timeline.
|
|
*
|
|
* @param {String} name the name to give the current mark
|
|
* @return {void}
|
|
*/
|
|
mark: function mark(str) {
|
|
this._perf.mark(str);
|
|
},
|
|
|
|
/**
|
|
* Calls the underlying getEntriesByName on the appropriate Window.performance
|
|
* object.
|
|
*
|
|
* @param {String} name
|
|
* @param {String} type eg "mark"
|
|
* @return {Array} Performance* objects
|
|
*/
|
|
getEntriesByName: function getEntriesByName(name, type) {
|
|
return this._perf.getEntriesByName(name, type);
|
|
},
|
|
|
|
/**
|
|
* The timeOrigin property from the appropriate performance object.
|
|
* Used to ensure that timestamps from the add-on code and the content code
|
|
* are comparable.
|
|
*
|
|
* @note If this is called from a context without a window
|
|
* (eg a JSM in chrome), it will return the timeOrigin of the XUL hidden
|
|
* window, which appears to be the first created window (and thus
|
|
* timeOrigin) in the browser. Note also, however, there is also a private
|
|
* hidden window, presumably for private browsing, which appears to be
|
|
* created dynamically later. Exactly how/when that shows up needs to be
|
|
* investigated.
|
|
*
|
|
* @return {Number} A double of milliseconds with a precision of 0.5us.
|
|
*/
|
|
get timeOrigin() {
|
|
return this._perf.timeOrigin;
|
|
},
|
|
|
|
/**
|
|
* Returns the "absolute" version of performance.now(), i.e. one that
|
|
* should ([bug 1401406](https://bugzilla.mozilla.org/show_bug.cgi?id=1401406)
|
|
* be comparable across both chrome and content.
|
|
*
|
|
* @return {Number}
|
|
*/
|
|
absNow: function absNow() {
|
|
return this.timeOrigin + this._perf.now();
|
|
},
|
|
|
|
/**
|
|
* This returns the absolute startTime from the most recent performance.mark()
|
|
* with the given name.
|
|
*
|
|
* @param {String} name the name to lookup the start time for
|
|
*
|
|
* @return {Number} the returned start time, as a DOMHighResTimeStamp
|
|
*
|
|
* @throws {Error} "No Marks with the name ..." if none are available
|
|
*
|
|
* @note Always surround calls to this by try/catch. Otherwise your code
|
|
* may fail when the `privacy.resistFingerprinting` pref is true. When
|
|
* this pref is set, all attempts to get marks will likely fail, which will
|
|
* cause this method to throw.
|
|
*
|
|
* See [bug 1369303](https://bugzilla.mozilla.org/show_bug.cgi?id=1369303)
|
|
* for more info.
|
|
*/
|
|
getMostRecentAbsMarkStartByName(name) {
|
|
let entries = this.getEntriesByName(name, "mark");
|
|
|
|
if (!entries.length) {
|
|
throw new Error(`No marks with the name ${name}`);
|
|
}
|
|
|
|
let mostRecentEntry = entries[entries.length - 1];
|
|
return this._perf.timeOrigin + mostRecentEntry.startTime;
|
|
}
|
|
};
|
|
|
|
var perfService = new _PerfService();
|
|
|
|
/***/ }),
|
|
/* 32 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MoreRecommendations", function() { return MoreRecommendations; });
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(13);
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_0__);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(9);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__);
|
|
|
|
|
|
|
|
class MoreRecommendations extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureComponent {
|
|
render() {
|
|
const { read_more_endpoint } = this.props;
|
|
if (read_more_endpoint) {
|
|
return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"a",
|
|
{ className: "more-recommendations", href: read_more_endpoint },
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_0__["FormattedMessage"], { id: "pocket_more_reccommendations" })
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/***/ }),
|
|
/* 33 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_PocketLoggedInCta", function() { return _PocketLoggedInCta; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "PocketLoggedInCta", function() { return PocketLoggedInCta; });
|
|
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(16);
|
|
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_0__);
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(13);
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_1__);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(9);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_2__);
|
|
|
|
|
|
|
|
|
|
class _PocketLoggedInCta extends react__WEBPACK_IMPORTED_MODULE_2___default.a.PureComponent {
|
|
render() {
|
|
const { pocketCta } = this.props.Pocket;
|
|
return react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(
|
|
"span",
|
|
{ className: "pocket-logged-in-cta" },
|
|
react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(
|
|
"a",
|
|
{ className: "pocket-cta-button", href: pocketCta.ctaUrl ? pocketCta.ctaUrl : "https://getpocket.com/" },
|
|
pocketCta.ctaButton ? pocketCta.ctaButton : react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__["FormattedMessage"], { id: "pocket_cta_button" })
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(
|
|
"a",
|
|
{ href: pocketCta.ctaUrl ? pocketCta.ctaUrl : "https://getpocket.com/" },
|
|
react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(
|
|
"span",
|
|
{ className: "cta-text" },
|
|
pocketCta.ctaText ? pocketCta.ctaText : react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__["FormattedMessage"], { id: "pocket_cta_text" })
|
|
)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
const PocketLoggedInCta = Object(react_redux__WEBPACK_IMPORTED_MODULE_0__["connect"])(state => ({ Pocket: state.Pocket }))(_PocketLoggedInCta);
|
|
|
|
/***/ }),
|
|
/* 34 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Topic", function() { return Topic; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Topics", function() { return Topics; });
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(13);
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_0__);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(9);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__);
|
|
|
|
|
|
|
|
class Topic extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureComponent {
|
|
render() {
|
|
const { url, name } = this.props;
|
|
return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"li",
|
|
null,
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"a",
|
|
{ key: name, href: url },
|
|
name
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
class Topics extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureComponent {
|
|
render() {
|
|
const { topics } = this.props;
|
|
return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"span",
|
|
{ className: "topics" },
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"span",
|
|
null,
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_0__["FormattedMessage"], { id: "pocket_read_more" })
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
|
"ul",
|
|
null,
|
|
topics && topics.map(t => react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(Topic, { key: t.name, url: t.url, name: t.name }))
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
/***/ }),
|
|
/* 35 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_TopSites", function() { return _TopSites; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TopSites", function() { return TopSites; });
|
|
/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
|
|
/* harmony import */ var _TopSitesConstants__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(36);
|
|
/* harmony import */ var content_src_components_CollapsibleSection_CollapsibleSection__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(27);
|
|
/* harmony import */ var content_src_components_ComponentPerfTimer_ComponentPerfTimer__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(30);
|
|
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(16);
|
|
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_4__);
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(13);
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_5__);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(9);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_6__);
|
|
/* harmony import */ var _SearchShortcutsForm__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(37);
|
|
/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(45);
|
|
/* harmony import */ var _TopSiteForm__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(48);
|
|
/* harmony import */ var _TopSite__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(38);
|
|
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function topSiteIconType(link) {
|
|
if (link.customScreenshotURL) {
|
|
return "custom_screenshot";
|
|
}
|
|
if (link.tippyTopIcon || link.faviconRef === "tippytop") {
|
|
return "tippytop";
|
|
}
|
|
if (link.faviconSize >= _TopSitesConstants__WEBPACK_IMPORTED_MODULE_1__["MIN_RICH_FAVICON_SIZE"]) {
|
|
return "rich_icon";
|
|
}
|
|
if (link.screenshot && link.faviconSize >= _TopSitesConstants__WEBPACK_IMPORTED_MODULE_1__["MIN_CORNER_FAVICON_SIZE"]) {
|
|
return "screenshot_with_icon";
|
|
}
|
|
if (link.screenshot) {
|
|
return "screenshot";
|
|
}
|
|
return "no_image";
|
|
}
|
|
|
|
/**
|
|
* Iterates through TopSites and counts types of images.
|
|
* @param acc Accumulator for reducer.
|
|
* @param topsite Entry in TopSites.
|
|
*/
|
|
function countTopSitesIconsTypes(topSites) {
|
|
const countTopSitesTypes = (acc, link) => {
|
|
acc[topSiteIconType(link)]++;
|
|
return acc;
|
|
};
|
|
|
|
return topSites.reduce(countTopSitesTypes, {
|
|
"custom_screenshot": 0,
|
|
"screenshot_with_icon": 0,
|
|
"screenshot": 0,
|
|
"tippytop": 0,
|
|
"rich_icon": 0,
|
|
"no_image": 0
|
|
});
|
|
}
|
|
|
|
class _TopSites extends react__WEBPACK_IMPORTED_MODULE_6___default.a.PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
this.onEditFormClose = this.onEditFormClose.bind(this);
|
|
this.onSearchShortcutsFormClose = this.onSearchShortcutsFormClose.bind(this);
|
|
}
|
|
|
|
/**
|
|
* Dispatch session statistics about the quality of TopSites icons and pinned count.
|
|
*/
|
|
_dispatchTopSitesStats() {
|
|
const topSites = this._getVisibleTopSites();
|
|
const topSitesIconsStats = countTopSitesIconsTypes(topSites);
|
|
const topSitesPinned = topSites.filter(site => !!site.isPinned).length;
|
|
const searchShortcuts = topSites.filter(site => !!site.searchTopSite).length;
|
|
// Dispatch telemetry event with the count of TopSites images types.
|
|
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({
|
|
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].SAVE_SESSION_PERF_DATA,
|
|
data: {
|
|
topsites_icon_stats: topSitesIconsStats,
|
|
topsites_pinned: topSitesPinned,
|
|
topsites_search_shortcuts: searchShortcuts
|
|
}
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Return the TopSites that are visible based on prefs and window width.
|
|
*/
|
|
_getVisibleTopSites() {
|
|
// We hide 2 sites per row when not in the wide layout.
|
|
let sitesPerRow = common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_8__["TOP_SITES_MAX_SITES_PER_ROW"];
|
|
// $break-point-widest = 1072px (from _variables.scss)
|
|
if (!global.matchMedia(`(min-width: 1072px)`).matches) {
|
|
sitesPerRow -= 2;
|
|
}
|
|
return this.props.TopSites.rows.slice(0, this.props.TopSitesRows * sitesPerRow);
|
|
}
|
|
|
|
componentDidUpdate() {
|
|
this._dispatchTopSitesStats();
|
|
}
|
|
|
|
componentDidMount() {
|
|
this._dispatchTopSitesStats();
|
|
}
|
|
|
|
onEditFormClose() {
|
|
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent({
|
|
source: _TopSitesConstants__WEBPACK_IMPORTED_MODULE_1__["TOP_SITES_SOURCE"],
|
|
event: "TOP_SITES_EDIT_CLOSE"
|
|
}));
|
|
this.props.dispatch({ type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].TOP_SITES_CANCEL_EDIT });
|
|
}
|
|
|
|
onSearchShortcutsFormClose() {
|
|
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent({
|
|
source: _TopSitesConstants__WEBPACK_IMPORTED_MODULE_1__["TOP_SITES_SOURCE"],
|
|
event: "SEARCH_EDIT_CLOSE"
|
|
}));
|
|
this.props.dispatch({ type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL });
|
|
}
|
|
|
|
render() {
|
|
const { props } = this;
|
|
const { editForm, showSearchShortcutsForm } = props.TopSites;
|
|
const extraMenuOptions = ["AddTopSite"];
|
|
if (props.Prefs.values["improvesearch.topSiteSearchShortcuts"]) {
|
|
extraMenuOptions.push("AddSearchShortcut");
|
|
}
|
|
|
|
return react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(
|
|
content_src_components_ComponentPerfTimer_ComponentPerfTimer__WEBPACK_IMPORTED_MODULE_3__["ComponentPerfTimer"],
|
|
{ id: "topsites", initialized: props.TopSites.initialized, dispatch: props.dispatch },
|
|
react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(
|
|
content_src_components_CollapsibleSection_CollapsibleSection__WEBPACK_IMPORTED_MODULE_2__["CollapsibleSection"],
|
|
{
|
|
className: "top-sites",
|
|
icon: "topsites",
|
|
id: "topsites",
|
|
title: { id: "header_top_sites" },
|
|
extraMenuOptions: extraMenuOptions,
|
|
showPrefName: "feeds.topsites",
|
|
eventSource: _TopSitesConstants__WEBPACK_IMPORTED_MODULE_1__["TOP_SITES_SOURCE"],
|
|
collapsed: props.TopSites.pref ? props.TopSites.pref.collapsed : undefined,
|
|
isFirst: props.isFirst,
|
|
isLast: props.isLast,
|
|
dispatch: props.dispatch },
|
|
react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(_TopSite__WEBPACK_IMPORTED_MODULE_10__["TopSiteList"], { TopSites: props.TopSites, TopSitesRows: props.TopSitesRows, dispatch: props.dispatch, intl: props.intl, topSiteIconType: topSiteIconType }),
|
|
react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(
|
|
"div",
|
|
{ className: "edit-topsites-wrapper" },
|
|
editForm && react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(
|
|
"div",
|
|
{ className: "edit-topsites" },
|
|
react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement("div", { className: "modal-overlay", onClick: this.onEditFormClose }),
|
|
react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(
|
|
"div",
|
|
{ className: "modal" },
|
|
react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(_TopSiteForm__WEBPACK_IMPORTED_MODULE_9__["TopSiteForm"], _extends({
|
|
site: props.TopSites.rows[editForm.index],
|
|
onClose: this.onEditFormClose,
|
|
dispatch: this.props.dispatch,
|
|
intl: this.props.intl
|
|
}, editForm))
|
|
)
|
|
),
|
|
showSearchShortcutsForm && react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(
|
|
"div",
|
|
{ className: "edit-search-shortcuts" },
|
|
react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement("div", { className: "modal-overlay", onClick: this.onSearchShortcutsFormClose }),
|
|
react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(
|
|
"div",
|
|
{ className: "modal" },
|
|
react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(_SearchShortcutsForm__WEBPACK_IMPORTED_MODULE_7__["SearchShortcutsForm"], {
|
|
TopSites: props.TopSites,
|
|
onClose: this.onSearchShortcutsFormClose,
|
|
dispatch: this.props.dispatch })
|
|
)
|
|
)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
const TopSites = Object(react_redux__WEBPACK_IMPORTED_MODULE_4__["connect"])(state => ({
|
|
TopSites: state.TopSites,
|
|
Prefs: state.Prefs,
|
|
TopSitesRows: state.Prefs.values.topSitesRows
|
|
}))(Object(react_intl__WEBPACK_IMPORTED_MODULE_5__["injectIntl"])(_TopSites));
|
|
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
|
|
|
|
/***/ }),
|
|
/* 36 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TOP_SITES_SOURCE", function() { return TOP_SITES_SOURCE; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TOP_SITES_CONTEXT_MENU_OPTIONS", function() { return TOP_SITES_CONTEXT_MENU_OPTIONS; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TOP_SITES_SEARCH_SHORTCUTS_CONTEXT_MENU_OPTIONS", function() { return TOP_SITES_SEARCH_SHORTCUTS_CONTEXT_MENU_OPTIONS; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MIN_RICH_FAVICON_SIZE", function() { return MIN_RICH_FAVICON_SIZE; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MIN_CORNER_FAVICON_SIZE", function() { return MIN_CORNER_FAVICON_SIZE; });
|
|
const TOP_SITES_SOURCE = "TOP_SITES";
|
|
const TOP_SITES_CONTEXT_MENU_OPTIONS = ["CheckPinTopSite", "EditTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "DeleteUrl"];
|
|
// the special top site for search shortcut experiment can only have the option to unpin (which removes) the topsite
|
|
const TOP_SITES_SEARCH_SHORTCUTS_CONTEXT_MENU_OPTIONS = ["CheckPinTopSite", "Separator", "BlockUrl"];
|
|
// minimum size necessary to show a rich icon instead of a screenshot
|
|
const MIN_RICH_FAVICON_SIZE = 96;
|
|
// minimum size necessary to show any icon in the top left corner with a screenshot
|
|
const MIN_CORNER_FAVICON_SIZE = 16;
|
|
|
|
/***/ }),
|
|
/* 37 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SearchShortcutsForm", function() { return SearchShortcutsForm; });
|
|
/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(13);
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_1__);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(9);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_2__);
|
|
/* harmony import */ var _TopSitesConstants__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(36);
|
|
|
|
|
|
|
|
|
|
|
|
class SelectableSearchShortcut extends react__WEBPACK_IMPORTED_MODULE_2___default.a.PureComponent {
|
|
render() {
|
|
const { shortcut, selected } = this.props;
|
|
const imageStyle = { backgroundImage: `url("${shortcut.tippyTopIcon}")` };
|
|
return react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(
|
|
"div",
|
|
{ className: "top-site-outer search-shortcut" },
|
|
react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("input", { type: "checkbox", id: shortcut.keyword, name: shortcut.keyword, checked: selected, onChange: this.props.onChange }),
|
|
react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(
|
|
"label",
|
|
{ htmlFor: shortcut.keyword },
|
|
react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(
|
|
"div",
|
|
{ className: "top-site-inner" },
|
|
react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(
|
|
"span",
|
|
null,
|
|
react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(
|
|
"div",
|
|
{ className: "tile" },
|
|
react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("div", { className: "top-site-icon rich-icon", style: imageStyle, "data-fallback": "@" }),
|
|
react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("div", { className: "top-site-icon search-topsite" })
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(
|
|
"div",
|
|
{ className: "title" },
|
|
react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(
|
|
"span",
|
|
{ dir: "auto" },
|
|
shortcut.keyword
|
|
)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
class SearchShortcutsForm extends react__WEBPACK_IMPORTED_MODULE_2___default.a.PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
this.handleChange = this.handleChange.bind(this);
|
|
this.onCancelButtonClick = this.onCancelButtonClick.bind(this);
|
|
this.onSaveButtonClick = this.onSaveButtonClick.bind(this);
|
|
|
|
// clone the shortcuts and add them to the state so we can add isSelected property
|
|
const shortcuts = [];
|
|
const { rows, searchShortcuts } = props.TopSites;
|
|
searchShortcuts.forEach(shortcut => {
|
|
shortcuts.push(Object.assign({}, shortcut, {
|
|
isSelected: !!rows.find(row => row && row.isPinned && row.searchTopSite && row.label === shortcut.keyword)
|
|
}));
|
|
});
|
|
this.state = { shortcuts };
|
|
}
|
|
|
|
handleChange(event) {
|
|
const { target } = event;
|
|
const { name, checked } = target;
|
|
this.setState(prevState => {
|
|
const shortcuts = prevState.shortcuts.slice();
|
|
let shortcut = shortcuts.find(({ keyword }) => keyword === name);
|
|
shortcut.isSelected = checked;
|
|
return { shortcuts };
|
|
});
|
|
}
|
|
|
|
onCancelButtonClick(ev) {
|
|
ev.preventDefault();
|
|
this.props.onClose();
|
|
}
|
|
|
|
onSaveButtonClick(ev) {
|
|
ev.preventDefault();
|
|
|
|
// Check if there were any changes and act accordingly
|
|
const { rows } = this.props.TopSites;
|
|
const pinQueue = [];
|
|
const unpinQueue = [];
|
|
this.state.shortcuts.forEach(shortcut => {
|
|
const alreadyPinned = rows.find(row => row && row.isPinned && row.searchTopSite && row.label === shortcut.keyword);
|
|
if (shortcut.isSelected && !alreadyPinned) {
|
|
pinQueue.push(this._searchTopSite(shortcut));
|
|
} else if (!shortcut.isSelected && alreadyPinned) {
|
|
unpinQueue.push({ url: alreadyPinned.url, searchVendor: shortcut.shortURL });
|
|
}
|
|
});
|
|
|
|
// Tell the feed to do the work.
|
|
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({
|
|
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].UPDATE_PINNED_SEARCH_SHORTCUTS,
|
|
data: {
|
|
addedShortcuts: pinQueue,
|
|
deletedShortcuts: unpinQueue
|
|
}
|
|
}));
|
|
|
|
// Send the Telemetry pings.
|
|
pinQueue.forEach(shortcut => {
|
|
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent({
|
|
source: _TopSitesConstants__WEBPACK_IMPORTED_MODULE_3__["TOP_SITES_SOURCE"],
|
|
event: "SEARCH_EDIT_ADD",
|
|
value: { search_vendor: shortcut.searchVendor }
|
|
}));
|
|
});
|
|
unpinQueue.forEach(shortcut => {
|
|
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent({
|
|
source: _TopSitesConstants__WEBPACK_IMPORTED_MODULE_3__["TOP_SITES_SOURCE"],
|
|
event: "SEARCH_EDIT_DELETE",
|
|
value: { search_vendor: shortcut.searchVendor }
|
|
}));
|
|
});
|
|
|
|
this.props.onClose();
|
|
}
|
|
|
|
_searchTopSite(shortcut) {
|
|
return {
|
|
url: shortcut.url,
|
|
searchTopSite: true,
|
|
label: shortcut.keyword,
|
|
searchVendor: shortcut.shortURL
|
|
};
|
|
}
|
|
|
|
render() {
|
|
return react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(
|
|
"form",
|
|
{ className: "topsite-form" },
|
|
react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(
|
|
"div",
|
|
{ className: "search-shortcuts-container" },
|
|
react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(
|
|
"h3",
|
|
{ className: "section-title" },
|
|
react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__["FormattedMessage"], { id: "section_menu_action_add_search_engine" })
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(
|
|
"div",
|
|
null,
|
|
this.state.shortcuts.map(shortcut => react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(SelectableSearchShortcut, { key: shortcut.keyword, shortcut: shortcut, selected: shortcut.isSelected, onChange: this.handleChange }))
|
|
)
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(
|
|
"section",
|
|
{ className: "actions" },
|
|
react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(
|
|
"button",
|
|
{ className: "cancel", type: "button", onClick: this.onCancelButtonClick },
|
|
react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__["FormattedMessage"], { id: "topsites_form_cancel_button" })
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(
|
|
"button",
|
|
{ className: "done", type: "submit", onClick: this.onSaveButtonClick },
|
|
react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__["FormattedMessage"], { id: "topsites_form_save_button" })
|
|
)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
/***/ }),
|
|
/* 38 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TopSiteLink", function() { return TopSiteLink; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TopSite", function() { return TopSite; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TopSitePlaceholder", function() { return TopSitePlaceholder; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_TopSiteList", function() { return _TopSiteList; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TopSiteList", function() { return TopSiteList; });
|
|
/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(13);
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_1__);
|
|
/* harmony import */ var _TopSitesConstants__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(36);
|
|
/* harmony import */ var content_src_components_LinkMenu_LinkMenu__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(24);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(9);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_4__);
|
|
/* harmony import */ var content_src_lib_screenshot_utils__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(26);
|
|
/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(45);
|
|
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TopSiteLink extends react__WEBPACK_IMPORTED_MODULE_4___default.a.PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = { screenshotImage: null };
|
|
this.onDragEvent = this.onDragEvent.bind(this);
|
|
this.onKeyPress = this.onKeyPress.bind(this);
|
|
}
|
|
|
|
/*
|
|
* Helper to determine whether the drop zone should allow a drop. We only allow
|
|
* dropping top sites for now.
|
|
*/
|
|
_allowDrop(e) {
|
|
return e.dataTransfer.types.includes("text/topsite-index");
|
|
}
|
|
|
|
onDragEvent(event) {
|
|
switch (event.type) {
|
|
case "click":
|
|
// Stop any link clicks if we started any dragging
|
|
if (this.dragged) {
|
|
event.preventDefault();
|
|
}
|
|
break;
|
|
case "dragstart":
|
|
this.dragged = true;
|
|
event.dataTransfer.effectAllowed = "move";
|
|
event.dataTransfer.setData("text/topsite-index", this.props.index);
|
|
event.target.blur();
|
|
this.props.onDragEvent(event, this.props.index, this.props.link, this.props.title);
|
|
break;
|
|
case "dragend":
|
|
this.props.onDragEvent(event);
|
|
break;
|
|
case "dragenter":
|
|
case "dragover":
|
|
case "drop":
|
|
if (this._allowDrop(event)) {
|
|
event.preventDefault();
|
|
this.props.onDragEvent(event, this.props.index);
|
|
}
|
|
break;
|
|
case "mousedown":
|
|
// Block the scroll wheel from appearing for middle clicks on search top sites
|
|
if (event.button === 1 && this.props.link.searchTopSite) {
|
|
event.preventDefault();
|
|
}
|
|
// Reset at the first mouse event of a potential drag
|
|
this.dragged = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper to obtain the next state based on nextProps and prevState.
|
|
*
|
|
* NOTE: Rename this method to getDerivedStateFromProps when we update React
|
|
* to >= 16.3. We will need to update tests as well. We cannot rename this
|
|
* method to getDerivedStateFromProps now because there is a mismatch in
|
|
* the React version that we are using for both testing and production.
|
|
* (i.e. react-test-render => "16.3.2", react => "16.2.0").
|
|
*
|
|
* See https://github.com/airbnb/enzyme/blob/master/packages/enzyme-adapter-react-16/package.json#L43.
|
|
*/
|
|
static getNextStateFromProps(nextProps, prevState) {
|
|
const { screenshot } = nextProps.link;
|
|
const imageInState = content_src_lib_screenshot_utils__WEBPACK_IMPORTED_MODULE_5__["ScreenshotUtils"].isRemoteImageLocal(prevState.screenshotImage, screenshot);
|
|
if (imageInState) {
|
|
return null;
|
|
}
|
|
|
|
// Since image was updated, attempt to revoke old image blob URL, if it exists.
|
|
content_src_lib_screenshot_utils__WEBPACK_IMPORTED_MODULE_5__["ScreenshotUtils"].maybeRevokeBlobObjectURL(prevState.screenshotImage);
|
|
|
|
return { screenshotImage: content_src_lib_screenshot_utils__WEBPACK_IMPORTED_MODULE_5__["ScreenshotUtils"].createLocalImageObject(screenshot) };
|
|
}
|
|
|
|
// NOTE: Remove this function when we update React to >= 16.3 since React will
|
|
// call getDerivedStateFromProps automatically. We will also need to
|
|
// rename getNextStateFromProps to getDerivedStateFromProps.
|
|
componentWillMount() {
|
|
const nextState = TopSiteLink.getNextStateFromProps(this.props, this.state);
|
|
if (nextState) {
|
|
this.setState(nextState);
|
|
}
|
|
}
|
|
|
|
// NOTE: Remove this function when we update React to >= 16.3 since React will
|
|
// call getDerivedStateFromProps automatically. We will also need to
|
|
// rename getNextStateFromProps to getDerivedStateFromProps.
|
|
componentWillReceiveProps(nextProps) {
|
|
const nextState = TopSiteLink.getNextStateFromProps(nextProps, this.state);
|
|
if (nextState) {
|
|
this.setState(nextState);
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
content_src_lib_screenshot_utils__WEBPACK_IMPORTED_MODULE_5__["ScreenshotUtils"].maybeRevokeBlobObjectURL(this.state.screenshotImage);
|
|
}
|
|
|
|
onKeyPress(event) {
|
|
// If we have tabbed to a search shortcut top site, and we click 'enter',
|
|
// we should execute the onClick function. This needs to be added because
|
|
// search top sites are anchor tags without an href. See bug 1483135
|
|
if (this.props.link.searchTopSite && event.key === "Enter") {
|
|
this.props.onClick(event);
|
|
}
|
|
}
|
|
|
|
render() {
|
|
const { children, className, defaultStyle, isDraggable, link, onClick, title } = this.props;
|
|
const topSiteOuterClassName = `top-site-outer${className ? ` ${className}` : ""}${link.isDragged ? " dragged" : ""}${link.searchTopSite ? " search-shortcut" : ""}`;
|
|
const { tippyTopIcon, faviconSize } = link;
|
|
const [letterFallback] = title;
|
|
let imageClassName;
|
|
let imageStyle;
|
|
let showSmallFavicon = false;
|
|
let smallFaviconStyle;
|
|
let smallFaviconFallback;
|
|
let hasScreenshotImage = this.state.screenshotImage && this.state.screenshotImage.url;
|
|
if (defaultStyle) {
|
|
// force no styles (letter fallback) even if the link has imagery
|
|
smallFaviconFallback = false;
|
|
} else if (link.searchTopSite) {
|
|
imageClassName = "top-site-icon rich-icon";
|
|
imageStyle = {
|
|
backgroundColor: link.backgroundColor,
|
|
backgroundImage: `url(${tippyTopIcon})`
|
|
};
|
|
smallFaviconStyle = { backgroundImage: `url(${tippyTopIcon})` };
|
|
} else if (link.customScreenshotURL) {
|
|
// assume high quality custom screenshot and use rich icon styles and class names
|
|
imageClassName = "top-site-icon rich-icon";
|
|
imageStyle = {
|
|
backgroundColor: link.backgroundColor,
|
|
backgroundImage: hasScreenshotImage ? `url(${this.state.screenshotImage.url})` : "none"
|
|
};
|
|
} else if (tippyTopIcon || faviconSize >= _TopSitesConstants__WEBPACK_IMPORTED_MODULE_2__["MIN_RICH_FAVICON_SIZE"]) {
|
|
// styles and class names for top sites with rich icons
|
|
imageClassName = "top-site-icon rich-icon";
|
|
imageStyle = {
|
|
backgroundColor: link.backgroundColor,
|
|
backgroundImage: `url(${tippyTopIcon || link.favicon})`
|
|
};
|
|
} else {
|
|
// styles and class names for top sites with screenshot + small icon in top left corner
|
|
imageClassName = `screenshot${hasScreenshotImage ? " active" : ""}`;
|
|
imageStyle = { backgroundImage: hasScreenshotImage ? `url(${this.state.screenshotImage.url})` : "none" };
|
|
|
|
// only show a favicon in top left if it's greater than 16x16
|
|
if (faviconSize >= _TopSitesConstants__WEBPACK_IMPORTED_MODULE_2__["MIN_CORNER_FAVICON_SIZE"]) {
|
|
showSmallFavicon = true;
|
|
smallFaviconStyle = { backgroundImage: `url(${link.favicon})` };
|
|
} else if (hasScreenshotImage) {
|
|
// Don't show a small favicon if there is no screenshot, because that
|
|
// would result in two fallback icons
|
|
showSmallFavicon = true;
|
|
smallFaviconFallback = true;
|
|
}
|
|
}
|
|
let draggableProps = {};
|
|
if (isDraggable) {
|
|
draggableProps = {
|
|
onClick: this.onDragEvent,
|
|
onDragEnd: this.onDragEvent,
|
|
onDragStart: this.onDragEvent,
|
|
onMouseDown: this.onDragEvent
|
|
};
|
|
}
|
|
return react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(
|
|
"li",
|
|
_extends({ className: topSiteOuterClassName, onDrop: this.onDragEvent, onDragOver: this.onDragEvent, onDragEnter: this.onDragEvent, onDragLeave: this.onDragEvent }, draggableProps),
|
|
react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(
|
|
"div",
|
|
{ className: "top-site-inner" },
|
|
react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(
|
|
"a",
|
|
{ href: !link.searchTopSite && link.url, tabIndex: "0", onKeyPress: this.onKeyPress, onClick: onClick, draggable: true },
|
|
react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(
|
|
"div",
|
|
{ className: "tile", "aria-hidden": true, "data-fallback": letterFallback },
|
|
react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("div", { className: imageClassName, style: imageStyle }),
|
|
link.searchTopSite && react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("div", { className: "top-site-icon search-topsite" }),
|
|
showSmallFavicon && react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("div", {
|
|
className: "top-site-icon default-icon",
|
|
"data-fallback": smallFaviconFallback && letterFallback,
|
|
style: smallFaviconStyle })
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(
|
|
"div",
|
|
{ className: `title ${link.isPinned ? "pinned" : ""}` },
|
|
link.isPinned && react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("div", { className: "icon icon-pin-small" }),
|
|
react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(
|
|
"span",
|
|
{ dir: "auto" },
|
|
title
|
|
)
|
|
)
|
|
),
|
|
children
|
|
)
|
|
);
|
|
}
|
|
}
|
|
TopSiteLink.defaultProps = {
|
|
title: "",
|
|
link: {},
|
|
isDraggable: true
|
|
};
|
|
|
|
class TopSite extends react__WEBPACK_IMPORTED_MODULE_4___default.a.PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = { showContextMenu: false };
|
|
this.onLinkClick = this.onLinkClick.bind(this);
|
|
this.onMenuButtonClick = this.onMenuButtonClick.bind(this);
|
|
this.onMenuUpdate = this.onMenuUpdate.bind(this);
|
|
}
|
|
|
|
/**
|
|
* Report to telemetry additional information about the item.
|
|
*/
|
|
_getTelemetryInfo() {
|
|
const value = { icon_type: this.props.link.iconType };
|
|
// Filter out "not_pinned" type for being the default
|
|
if (this.props.link.isPinned) {
|
|
value.card_type = "pinned";
|
|
}
|
|
if (this.props.link.searchTopSite) {
|
|
// Set the card_type as "search" regardless of its pinning status
|
|
value.card_type = "search";
|
|
value.search_vendor = this.props.link.hostname;
|
|
}
|
|
return { value };
|
|
}
|
|
|
|
userEvent(event) {
|
|
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent(Object.assign({
|
|
event,
|
|
source: _TopSitesConstants__WEBPACK_IMPORTED_MODULE_2__["TOP_SITES_SOURCE"],
|
|
action_position: this.props.index
|
|
}, this._getTelemetryInfo())));
|
|
}
|
|
|
|
onLinkClick(event) {
|
|
this.userEvent("CLICK");
|
|
|
|
// Specially handle a top site link click for "typed" frecency bonus as
|
|
// specified as a property on the link.
|
|
event.preventDefault();
|
|
const { altKey, button, ctrlKey, metaKey, shiftKey } = event;
|
|
if (!this.props.link.searchTopSite) {
|
|
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({
|
|
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].OPEN_LINK,
|
|
data: Object.assign(this.props.link, { event: { altKey, button, ctrlKey, metaKey, shiftKey } })
|
|
}));
|
|
} else {
|
|
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({
|
|
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].FILL_SEARCH_TERM,
|
|
data: { label: this.props.link.label }
|
|
}));
|
|
}
|
|
}
|
|
|
|
onMenuButtonClick(event) {
|
|
event.preventDefault();
|
|
this.props.onActivate(this.props.index);
|
|
this.setState({ showContextMenu: true });
|
|
}
|
|
|
|
onMenuUpdate(showContextMenu) {
|
|
this.setState({ showContextMenu });
|
|
}
|
|
|
|
render() {
|
|
const { props } = this;
|
|
const { link } = props;
|
|
const isContextMenuOpen = this.state.showContextMenu && props.activeIndex === props.index;
|
|
const title = link.label || link.hostname;
|
|
return react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(
|
|
TopSiteLink,
|
|
_extends({}, props, { onClick: this.onLinkClick, onDragEvent: this.props.onDragEvent, className: `${props.className || ""}${isContextMenuOpen ? " active" : ""}`, title: title }),
|
|
react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(
|
|
"div",
|
|
null,
|
|
react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(
|
|
"button",
|
|
{ className: "context-menu-button icon", title: this.props.intl.formatMessage({ id: "context_menu_title" }), onClick: this.onMenuButtonClick },
|
|
react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(
|
|
"span",
|
|
{ className: "sr-only" },
|
|
react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__["FormattedMessage"], { id: "context_menu_button_sr", values: { title } })
|
|
)
|
|
),
|
|
isContextMenuOpen && react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(content_src_components_LinkMenu_LinkMenu__WEBPACK_IMPORTED_MODULE_3__["LinkMenu"], {
|
|
dispatch: props.dispatch,
|
|
index: props.index,
|
|
onUpdate: this.onMenuUpdate,
|
|
options: link.searchTopSite ? _TopSitesConstants__WEBPACK_IMPORTED_MODULE_2__["TOP_SITES_SEARCH_SHORTCUTS_CONTEXT_MENU_OPTIONS"] : _TopSitesConstants__WEBPACK_IMPORTED_MODULE_2__["TOP_SITES_CONTEXT_MENU_OPTIONS"],
|
|
site: link,
|
|
siteInfo: this._getTelemetryInfo(),
|
|
source: _TopSitesConstants__WEBPACK_IMPORTED_MODULE_2__["TOP_SITES_SOURCE"] })
|
|
)
|
|
);
|
|
}
|
|
}
|
|
TopSite.defaultProps = {
|
|
link: {},
|
|
onActivate() {}
|
|
};
|
|
|
|
class TopSitePlaceholder extends react__WEBPACK_IMPORTED_MODULE_4___default.a.PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
this.onEditButtonClick = this.onEditButtonClick.bind(this);
|
|
}
|
|
|
|
onEditButtonClick() {
|
|
this.props.dispatch({ type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].TOP_SITES_EDIT, data: { index: this.props.index } });
|
|
}
|
|
|
|
render() {
|
|
return react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(
|
|
TopSiteLink,
|
|
_extends({}, this.props, { className: `placeholder ${this.props.className || ""}`, isDraggable: false }),
|
|
react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("button", { className: "context-menu-button edit-button icon",
|
|
title: this.props.intl.formatMessage({ id: "edit_topsites_edit_button" }),
|
|
onClick: this.onEditButtonClick })
|
|
);
|
|
}
|
|
}
|
|
|
|
class _TopSiteList extends react__WEBPACK_IMPORTED_MODULE_4___default.a.PureComponent {
|
|
static get DEFAULT_STATE() {
|
|
return {
|
|
activeIndex: null,
|
|
draggedIndex: null,
|
|
draggedSite: null,
|
|
draggedTitle: null,
|
|
topSitesPreview: null
|
|
};
|
|
}
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = _TopSiteList.DEFAULT_STATE;
|
|
this.onDragEvent = this.onDragEvent.bind(this);
|
|
this.onActivate = this.onActivate.bind(this);
|
|
}
|
|
|
|
componentWillReceiveProps(nextProps) {
|
|
if (this.state.draggedSite) {
|
|
const prevTopSites = this.props.TopSites && this.props.TopSites.rows;
|
|
const newTopSites = nextProps.TopSites && nextProps.TopSites.rows;
|
|
if (prevTopSites && prevTopSites[this.state.draggedIndex] && prevTopSites[this.state.draggedIndex].url === this.state.draggedSite.url && (!newTopSites[this.state.draggedIndex] || newTopSites[this.state.draggedIndex].url !== this.state.draggedSite.url)) {
|
|
// We got the new order from the redux store via props. We can clear state now.
|
|
this.setState(_TopSiteList.DEFAULT_STATE);
|
|
}
|
|
}
|
|
}
|
|
|
|
userEvent(event, index) {
|
|
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent({
|
|
event,
|
|
source: _TopSitesConstants__WEBPACK_IMPORTED_MODULE_2__["TOP_SITES_SOURCE"],
|
|
action_position: index
|
|
}));
|
|
}
|
|
|
|
onDragEvent(event, index, link, title) {
|
|
switch (event.type) {
|
|
case "dragstart":
|
|
this.dropped = false;
|
|
this.setState({
|
|
draggedIndex: index,
|
|
draggedSite: link,
|
|
draggedTitle: title,
|
|
activeIndex: null
|
|
});
|
|
this.userEvent("DRAG", index);
|
|
break;
|
|
case "dragend":
|
|
if (!this.dropped) {
|
|
// If there was no drop event, reset the state to the default.
|
|
this.setState(_TopSiteList.DEFAULT_STATE);
|
|
}
|
|
break;
|
|
case "dragenter":
|
|
if (index === this.state.draggedIndex) {
|
|
this.setState({ topSitesPreview: null });
|
|
} else {
|
|
this.setState({ topSitesPreview: this._makeTopSitesPreview(index) });
|
|
}
|
|
break;
|
|
case "drop":
|
|
if (index !== this.state.draggedIndex) {
|
|
this.dropped = true;
|
|
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({
|
|
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].TOP_SITES_INSERT,
|
|
data: {
|
|
site: Object.assign({
|
|
url: this.state.draggedSite.url,
|
|
label: this.state.draggedTitle,
|
|
customScreenshotURL: this.state.draggedSite.customScreenshotURL
|
|
}, this.state.draggedSite.searchTopSite && { searchTopSite: true }),
|
|
index,
|
|
draggedFromIndex: this.state.draggedIndex
|
|
}
|
|
}));
|
|
this.userEvent("DROP", index);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
_getTopSites() {
|
|
// Make a copy of the sites to truncate or extend to desired length
|
|
let topSites = this.props.TopSites.rows.slice();
|
|
topSites.length = this.props.TopSitesRows * common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_6__["TOP_SITES_MAX_SITES_PER_ROW"];
|
|
return topSites;
|
|
}
|
|
|
|
/**
|
|
* Make a preview of the topsites that will be the result of dropping the currently
|
|
* dragged site at the specified index.
|
|
*/
|
|
_makeTopSitesPreview(index) {
|
|
const topSites = this._getTopSites();
|
|
topSites[this.state.draggedIndex] = null;
|
|
const pinnedOnly = topSites.map(site => site && site.isPinned ? site : null);
|
|
const unpinned = topSites.filter(site => site && !site.isPinned);
|
|
const siteToInsert = Object.assign({}, this.state.draggedSite, { isPinned: true, isDragged: true });
|
|
if (!pinnedOnly[index]) {
|
|
pinnedOnly[index] = siteToInsert;
|
|
} else {
|
|
// Find the hole to shift the pinned site(s) towards. We shift towards the
|
|
// hole left by the site being dragged.
|
|
let holeIndex = index;
|
|
const indexStep = index > this.state.draggedIndex ? -1 : 1;
|
|
while (pinnedOnly[holeIndex]) {
|
|
holeIndex += indexStep;
|
|
}
|
|
|
|
// Shift towards the hole.
|
|
const shiftingStep = index > this.state.draggedIndex ? 1 : -1;
|
|
while (holeIndex !== index) {
|
|
const nextIndex = holeIndex + shiftingStep;
|
|
pinnedOnly[holeIndex] = pinnedOnly[nextIndex];
|
|
holeIndex = nextIndex;
|
|
}
|
|
pinnedOnly[index] = siteToInsert;
|
|
}
|
|
|
|
// Fill in the remaining holes with unpinned sites.
|
|
const preview = pinnedOnly;
|
|
for (let i = 0; i < preview.length; i++) {
|
|
if (!preview[i]) {
|
|
preview[i] = unpinned.shift() || null;
|
|
}
|
|
}
|
|
|
|
return preview;
|
|
}
|
|
|
|
onActivate(index) {
|
|
this.setState({ activeIndex: index });
|
|
}
|
|
|
|
render() {
|
|
const { props } = this;
|
|
const topSites = this.state.topSitesPreview || this._getTopSites();
|
|
const topSitesUI = [];
|
|
const commonProps = {
|
|
onDragEvent: this.onDragEvent,
|
|
dispatch: props.dispatch,
|
|
intl: props.intl
|
|
};
|
|
// We assign a key to each placeholder slot. We need it to be independent
|
|
// of the slot index (i below) so that the keys used stay the same during
|
|
// drag and drop reordering and the underlying DOM nodes are reused.
|
|
// This mostly (only?) affects linux so be sure to test on linux before changing.
|
|
let holeIndex = 0;
|
|
|
|
// On narrow viewports, we only show 6 sites per row. We'll mark the rest as
|
|
// .hide-for-narrow to hide in CSS via @media query.
|
|
const maxNarrowVisibleIndex = props.TopSitesRows * 6;
|
|
|
|
for (let i = 0, l = topSites.length; i < l; i++) {
|
|
const link = topSites[i] && Object.assign({}, topSites[i], { iconType: this.props.topSiteIconType(topSites[i]) });
|
|
const slotProps = {
|
|
key: link ? link.url : holeIndex++,
|
|
index: i
|
|
};
|
|
if (i >= maxNarrowVisibleIndex) {
|
|
slotProps.className = "hide-for-narrow";
|
|
}
|
|
topSitesUI.push(!link ? react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(TopSitePlaceholder, _extends({}, slotProps, commonProps)) : react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(TopSite, _extends({
|
|
link: link,
|
|
activeIndex: this.state.activeIndex,
|
|
onActivate: this.onActivate
|
|
}, slotProps, commonProps)));
|
|
}
|
|
return react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(
|
|
"ul",
|
|
{ className: `top-sites-list${this.state.draggedSite ? " dnd-active" : ""}` },
|
|
topSitesUI
|
|
);
|
|
}
|
|
}
|
|
|
|
const TopSiteList = Object(react_intl__WEBPACK_IMPORTED_MODULE_1__["injectIntl"])(_TopSiteList);
|
|
|
|
/***/ }),
|
|
/* 39 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_StartupOverlay", function() { return _StartupOverlay; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "StartupOverlay", function() { return StartupOverlay; });
|
|
/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(13);
|
|
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_1__);
|
|
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(16);
|
|
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_2__);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(9);
|
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_3__);
|
|
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _StartupOverlay extends react__WEBPACK_IMPORTED_MODULE_3___default.a.PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
this.onInputChange = this.onInputChange.bind(this);
|
|
this.onSubmit = this.onSubmit.bind(this);
|
|
this.clickSkip = this.clickSkip.bind(this);
|
|
this.initScene = this.initScene.bind(this);
|
|
this.removeOverlay = this.removeOverlay.bind(this);
|
|
this.onInputInvalid = this.onInputInvalid.bind(this);
|
|
|
|
this.state = {
|
|
emailInput: "",
|
|
overlayRemoved: false,
|
|
flowId: "",
|
|
flowBeginTime: 0
|
|
};
|
|
this.didFetch = false;
|
|
}
|
|
|
|
componentWillUpdate() {
|
|
var _this = this;
|
|
|
|
return _asyncToGenerator(function* () {
|
|
if (_this.props.fxa_endpoint && !_this.didFetch) {
|
|
try {
|
|
_this.didFetch = true;
|
|
const fxaParams = "entrypoint=activity-stream-firstrun&utm_source=activity-stream&utm_campaign=firstrun&form_type=email";
|
|
const response = yield fetch(`${_this.props.fxa_endpoint}/metrics-flow?${fxaParams}`);
|
|
if (response.status === 200) {
|
|
const { flowId, flowBeginTime } = yield response.json();
|
|
_this.setState({ flowId, flowBeginTime });
|
|
} else {
|
|
_this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({ type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].TELEMETRY_UNDESIRED_EVENT, data: { event: "FXA_METRICS_FETCH_ERROR", value: response.status } }));
|
|
}
|
|
} catch (error) {
|
|
_this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({ type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].TELEMETRY_UNDESIRED_EVENT, data: { event: "FXA_METRICS_ERROR" } }));
|
|
}
|
|
}
|
|
})();
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.initScene();
|
|
}
|
|
|
|
initScene() {
|
|
// Timeout to allow the scene to render once before attaching the attribute
|
|
// to trigger the animation.
|
|
setTimeout(() => {
|
|
this.setState({ show: true });
|
|
}, 10);
|
|
}
|
|
|
|
removeOverlay() {
|
|
window.removeEventListener("visibilitychange", this.removeOverlay);
|
|
document.body.classList.remove("hide-main");
|
|
this.setState({ show: false });
|
|
setTimeout(() => {
|
|
// Allow scrolling and fully remove overlay after animation finishes.
|
|
document.body.classList.remove("welcome");
|
|
this.setState({ overlayRemoved: true });
|
|
}, 400);
|
|
}
|
|
|
|
onInputChange(e) {
|
|
let error = e.target.previousSibling;
|
|
this.setState({ emailInput: e.target.value });
|
|
error.classList.remove("active");
|
|
e.target.classList.remove("invalid");
|
|
}
|
|
|
|
onSubmit() {
|
|
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent(Object.assign({ event: "SUBMIT_EMAIL" }, this._getFormInfo())));
|
|
|
|
window.addEventListener("visibilitychange", this.removeOverlay);
|
|
}
|
|
|
|
clickSkip() {
|
|
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent(Object.assign({ event: "SKIPPED_SIGNIN" }, this._getFormInfo())));
|
|
this.removeOverlay();
|
|
}
|
|
|
|
/**
|
|
* Report to telemetry additional information about the form submission.
|
|
*/
|
|
_getFormInfo() {
|
|
const value = { has_flow_params: this.state.flowId.length > 0 };
|
|
return { value };
|
|
}
|
|
|
|
onInputInvalid(e) {
|
|
let error = e.target.previousSibling;
|
|
error.classList.add("active");
|
|
e.target.classList.add("invalid");
|
|
e.preventDefault(); // Override built-in form validation popup
|
|
e.target.focus();
|
|
}
|
|
|
|
render() {
|
|
// When skipping the onboarding tour we show AS but we are still on
|
|
// about:welcome, prop.isFirstrun is true and StartupOverlay is rendered
|
|
if (this.state.overlayRemoved) {
|
|
return null;
|
|
}
|
|
|
|
let termsLink = react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"a",
|
|
{ href: `${this.props.fxa_endpoint}/legal/terms`, target: "_blank", rel: "noopener noreferrer" },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__["FormattedMessage"], { id: "firstrun_terms_of_service" })
|
|
);
|
|
let privacyLink = react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"a",
|
|
{ href: `${this.props.fxa_endpoint}/legal/privacy`, target: "_blank", rel: "noopener noreferrer" },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__["FormattedMessage"], { id: "firstrun_privacy_notice" })
|
|
);
|
|
|
|
return react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"div",
|
|
{ className: `overlay-wrapper ${this.state.show ? "show" : ""}` },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("div", { className: "background" }),
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"div",
|
|
{ className: "firstrun-scene" },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"div",
|
|
{ className: "fxaccounts-container" },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"div",
|
|
{ className: "firstrun-left-divider" },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"h1",
|
|
{ className: "firstrun-title" },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__["FormattedMessage"], { id: "firstrun_title" })
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"p",
|
|
{ className: "firstrun-content" },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__["FormattedMessage"], { id: "firstrun_content" })
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"a",
|
|
{ className: "firstrun-link", href: "https://www.mozilla.org/firefox/features/sync/", target: "_blank", rel: "noopener noreferrer" },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__["FormattedMessage"], { id: "firstrun_learn_more_link" })
|
|
)
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"div",
|
|
{ className: "firstrun-sign-in" },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"p",
|
|
{ className: "form-header" },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__["FormattedMessage"], { id: "firstrun_form_header" }),
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"span",
|
|
{ className: "sub-header" },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__["FormattedMessage"], { id: "firstrun_form_sub_header" })
|
|
)
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"form",
|
|
{ method: "get", action: this.props.fxa_endpoint, target: "_blank", rel: "noopener noreferrer", onSubmit: this.onSubmit },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", { name: "service", type: "hidden", value: "sync" }),
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", { name: "action", type: "hidden", value: "email" }),
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", { name: "context", type: "hidden", value: "fx_desktop_v3" }),
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", { name: "entrypoint", type: "hidden", value: "activity-stream-firstrun" }),
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", { name: "utm_source", type: "hidden", value: "activity-stream" }),
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", { name: "utm_campaign", type: "hidden", value: "firstrun" }),
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", { name: "flow_id", type: "hidden", value: this.state.flowId }),
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", { name: "flow_begin_time", type: "hidden", value: this.state.flowBeginTime }),
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"span",
|
|
{ className: "error" },
|
|
this.props.intl.formatMessage({ id: "firstrun_invalid_input" })
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", { className: "email-input", name: "email", type: "email", required: "true", onInvalid: this.onInputInvalid, placeholder: this.props.intl.formatMessage({ id: "firstrun_email_input_placeholder" }), onChange: this.onInputChange }),
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"div",
|
|
{ className: "extra-links" },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__["FormattedMessage"], {
|
|
id: "firstrun_extra_legal_links",
|
|
values: {
|
|
terms: termsLink,
|
|
privacy: privacyLink
|
|
} })
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"button",
|
|
{ className: "continue-button", type: "submit" },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__["FormattedMessage"], { id: "firstrun_continue_to_login" })
|
|
)
|
|
),
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(
|
|
"button",
|
|
{ className: "skip-button", disabled: !!this.state.emailInput, onClick: this.clickSkip },
|
|
react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__["FormattedMessage"], { id: "firstrun_skip_login" })
|
|
)
|
|
)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
const getState = state => ({ fxa_endpoint: state.Prefs.values.fxa_endpoint });
|
|
const StartupOverlay = Object(react_redux__WEBPACK_IMPORTED_MODULE_2__["connect"])(getState)(Object(react_intl__WEBPACK_IMPORTED_MODULE_1__["injectIntl"])(_StartupOverlay));
|
|
|
|
/***/ }),
|
|
/* 40 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "DetectUserSessionStart", function() { return DetectUserSessionStart; });
|
|
/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
|
|
/* harmony import */ var common_PerfService_jsm__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(31);
|
|
|
|
|
|
|
|
const VISIBLE = "visible";
|
|
const VISIBILITY_CHANGE_EVENT = "visibilitychange";
|
|
|
|
class DetectUserSessionStart {
|
|
constructor(store, options = {}) {
|
|
this._store = store;
|
|
// Overrides for testing
|
|
this.document = options.document || global.document;
|
|
this._perfService = options.perfService || common_PerfService_jsm__WEBPACK_IMPORTED_MODULE_1__["perfService"];
|
|
this._onVisibilityChange = this._onVisibilityChange.bind(this);
|
|
}
|
|
|
|
/**
|
|
* sendEventOrAddListener - Notify immediately if the page is already visible,
|
|
* or else set up a listener for when visibility changes.
|
|
* This is needed for accurate session tracking for telemetry,
|
|
* because tabs are pre-loaded.
|
|
*/
|
|
sendEventOrAddListener() {
|
|
if (this.document.visibilityState === VISIBLE) {
|
|
// If the document is already visible, to the user, send a notification
|
|
// immediately that a session has started.
|
|
this._sendEvent();
|
|
} else {
|
|
// If the document is not visible, listen for when it does become visible.
|
|
this.document.addEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* _sendEvent - Sends a message to the main process to indicate the current
|
|
* tab is now visible to the user, includes the
|
|
* visibility_event_rcvd_ts time in ms from the UNIX epoch.
|
|
*/
|
|
_sendEvent() {
|
|
this._perfService.mark("visibility_event_rcvd_ts");
|
|
|
|
try {
|
|
let visibility_event_rcvd_ts = this._perfService.getMostRecentAbsMarkStartByName("visibility_event_rcvd_ts");
|
|
|
|
this._store.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({
|
|
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].SAVE_SESSION_PERF_DATA,
|
|
data: { visibility_event_rcvd_ts }
|
|
}));
|
|
} catch (ex) {
|
|
// If this failed, it's likely because the `privacy.resistFingerprinting`
|
|
// pref is true. We should at least not blow up.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* _onVisibilityChange - If the visibility has changed to visible, sends a notification
|
|
* and removes the event listener. This should only be called once per tab.
|
|
*/
|
|
_onVisibilityChange() {
|
|
if (this.document.visibilityState === VISIBLE) {
|
|
this._sendEvent();
|
|
this.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
|
|
}
|
|
}
|
|
}
|
|
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
|
|
|
|
/***/ }),
|
|
/* 41 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "enableASRouterContent", function() { return enableASRouterContent; });
|
|
function enableASRouterContent(store, asrouterContent) {
|
|
// Enable asrouter content
|
|
store.subscribe(() => {
|
|
const state = store.getState();
|
|
if (!state.ASRouter.initialized) {
|
|
return;
|
|
}
|
|
|
|
if (!asrouterContent.initialized) {
|
|
asrouterContent.init();
|
|
}
|
|
});
|
|
// Return this for testing purposes
|
|
return { asrouterContent };
|
|
}
|
|
|
|
/***/ }),
|
|
/* 42 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
|
|
// EXTERNAL MODULE: external "React"
|
|
var external_React_ = __webpack_require__(9);
|
|
var external_React_default = /*#__PURE__*/__webpack_require__.n(external_React_);
|
|
|
|
// CONCATENATED MODULE: ./content-src/asrouter/components/Button/Button.jsx
|
|
|
|
|
|
const ALLOWED_STYLE_TAGS = ["color", "backgroundColor"];
|
|
|
|
const Button = props => {
|
|
const style = {};
|
|
|
|
// Add allowed style tags from props, e.g. props.color becomes style={color: props.color}
|
|
for (const tag of ALLOWED_STYLE_TAGS) {
|
|
if (typeof props[tag] !== "undefined") {
|
|
style[tag] = props[tag];
|
|
}
|
|
}
|
|
// remove border if bg is set to something custom
|
|
if (style.backgroundColor) {
|
|
style.border = "0";
|
|
}
|
|
|
|
return external_React_default.a.createElement(
|
|
"button",
|
|
{ onClick: props.onClick,
|
|
className: props.className || "ASRouterButton",
|
|
style: style },
|
|
props.children
|
|
);
|
|
};
|
|
// EXTERNAL MODULE: ./node_modules/fluent-react/src/index.js + 7 modules
|
|
var src = __webpack_require__(44);
|
|
|
|
// EXTERNAL MODULE: ./content-src/asrouter/rich-text-strings.js
|
|
var rich_text_strings = __webpack_require__(7);
|
|
|
|
// CONCATENATED MODULE: ./content-src/asrouter/template-utils.js
|
|
function safeURI(url) {
|
|
if (!url) {
|
|
return "";
|
|
}
|
|
const { protocol } = new URL(url);
|
|
const isAllowed = ["http:", "https:", "data:", "resource:", "chrome:"].includes(protocol);
|
|
if (!isAllowed) {
|
|
console.warn(`The protocol ${protocol} is not allowed for template URLs.`); // eslint-disable-line no-console
|
|
}
|
|
return isAllowed ? url : "";
|
|
}
|
|
// CONCATENATED MODULE: ./content-src/asrouter/components/RichText/RichText.jsx
|
|
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Elements allowed in snippet content
|
|
const ALLOWED_TAGS = {
|
|
b: external_React_default.a.createElement("b", null),
|
|
i: external_React_default.a.createElement("i", null),
|
|
u: external_React_default.a.createElement("u", null),
|
|
strong: external_React_default.a.createElement("strong", null),
|
|
em: external_React_default.a.createElement("em", null),
|
|
br: external_React_default.a.createElement("br", null)
|
|
};
|
|
|
|
/**
|
|
* Transform an object (tag name: {url}) into (tag name: anchor) where the url
|
|
* is used as href, in order to render links inside a Fluent.Localized component.
|
|
*/
|
|
function convertLinks(links, sendClick, doNotAutoBlock) {
|
|
if (links) {
|
|
return Object.keys(links).reduce((acc, linkTag) => {
|
|
const { action } = links[linkTag];
|
|
// Setting the value to false will not include the attribute in the anchor
|
|
const url = action ? false : safeURI(links[linkTag].url);
|
|
|
|
acc[linkTag] = external_React_default.a.createElement("a", { href: url,
|
|
target: doNotAutoBlock ? "_blank" : "",
|
|
"data-metric": links[linkTag].metric,
|
|
"data-action": action,
|
|
"data-args": links[linkTag].args,
|
|
"data-do_not_autoblock": doNotAutoBlock,
|
|
onClick: sendClick });
|
|
return acc;
|
|
}, {});
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Message wrapper used to sanitize markup and render HTML.
|
|
*/
|
|
function RichText(props) {
|
|
if (!rich_text_strings["RICH_TEXT_KEYS"].includes(props.localization_id)) {
|
|
throw new Error(`ASRouter: ${props.localization_id} is not a valid rich text property. If you want it to be processed, you need to add it to asrouter/rich-text-strings.js`);
|
|
}
|
|
return external_React_default.a.createElement(
|
|
src["Localized"],
|
|
_extends({ id: props.localization_id }, ALLOWED_TAGS, props.customElements, convertLinks(props.links, props.sendClick, props.doNotAutoBlock)),
|
|
external_React_default.a.createElement(
|
|
"span",
|
|
null,
|
|
props.text
|
|
)
|
|
);
|
|
}
|
|
// CONCATENATED MODULE: ./content-src/asrouter/components/SnippetBase/SnippetBase.jsx
|
|
|
|
|
|
class SnippetBase_SnippetBase extends external_React_default.a.PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
this.onBlockClicked = this.onBlockClicked.bind(this);
|
|
}
|
|
|
|
onBlockClicked() {
|
|
if (this.props.provider !== "preview") {
|
|
this.props.sendUserActionTelemetry({ event: "BLOCK", id: this.props.UISurface });
|
|
}
|
|
|
|
this.props.onBlock();
|
|
}
|
|
|
|
renderDismissButton() {
|
|
if (this.props.footerDismiss) {
|
|
return external_React_default.a.createElement(
|
|
"div",
|
|
{ className: "footer" },
|
|
external_React_default.a.createElement(
|
|
"div",
|
|
{ className: "footer-content" },
|
|
external_React_default.a.createElement(
|
|
"button",
|
|
{
|
|
className: "ASRouterButton secondary",
|
|
onClick: this.props.onDismiss },
|
|
this.props.content.scene2_dismiss_button_text
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
return external_React_default.a.createElement("button", { className: "blockButton", title: this.props.content.block_button_text || "Remove this", onClick: this.onBlockClicked });
|
|
}
|
|
|
|
render() {
|
|
const { props } = this;
|
|
|
|
const containerClassName = `SnippetBaseContainer${props.className ? ` ${props.className}` : ""}`;
|
|
|
|
return external_React_default.a.createElement(
|
|
"div",
|
|
{ className: containerClassName, style: this.props.textStyle },
|
|
external_React_default.a.createElement(
|
|
"div",
|
|
{ className: "innerWrapper" },
|
|
props.children
|
|
),
|
|
this.renderDismissButton()
|
|
);
|
|
}
|
|
}
|
|
// CONCATENATED MODULE: ./content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.jsx
|
|
var SimpleSnippet_extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const DEFAULT_ICON_PATH = "chrome://branding/content/icon64.png";
|
|
|
|
class SimpleSnippet_SimpleSnippet extends external_React_default.a.PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
this.onButtonClick = this.onButtonClick.bind(this);
|
|
}
|
|
|
|
onButtonClick() {
|
|
if (this.props.provider !== "preview") {
|
|
this.props.sendUserActionTelemetry({ event: "CLICK_BUTTON", id: this.props.UISurface });
|
|
}
|
|
const { button_url } = this.props.content;
|
|
// If button_url is defined handle it as OPEN_URL action
|
|
const type = this.props.content.button_action || button_url && "OPEN_URL";
|
|
this.props.onAction({
|
|
type,
|
|
data: { args: this.props.content.button_action_args || button_url }
|
|
});
|
|
if (!this.props.content.do_not_autoblock) {
|
|
this.props.onBlock();
|
|
}
|
|
}
|
|
|
|
renderTitle() {
|
|
const { title } = this.props.content;
|
|
return title ? external_React_default.a.createElement(
|
|
"h3",
|
|
{ className: "title" },
|
|
title
|
|
) : null;
|
|
}
|
|
|
|
renderTitleIcon() {
|
|
const titleIcon = safeURI(this.props.content.title_icon);
|
|
return titleIcon ? external_React_default.a.createElement("span", { className: "titleIcon", style: { backgroundImage: `url("${titleIcon}")` } }) : null;
|
|
}
|
|
|
|
renderButton() {
|
|
const { props } = this;
|
|
if (!props.content.button_action && !props.onButtonClick && !props.content.button_url) {
|
|
return null;
|
|
}
|
|
|
|
return external_React_default.a.createElement(
|
|
Button,
|
|
{
|
|
onClick: props.onButtonClick || this.onButtonClick,
|
|
color: props.content.button_color,
|
|
backgroundColor: props.content.button_background_color },
|
|
props.content.button_label
|
|
);
|
|
}
|
|
|
|
renderText() {
|
|
const { props } = this;
|
|
return external_React_default.a.createElement(RichText, { text: props.content.text,
|
|
customElements: this.props.customElements,
|
|
localization_id: "text",
|
|
links: props.content.links,
|
|
sendClick: props.sendClick });
|
|
}
|
|
|
|
render() {
|
|
const { props } = this;
|
|
let className = "SimpleSnippet";
|
|
if (props.className) {
|
|
className += ` ${props.className}`;
|
|
}
|
|
if (props.content.tall) {
|
|
className += " tall";
|
|
}
|
|
return external_React_default.a.createElement(
|
|
SnippetBase_SnippetBase,
|
|
SimpleSnippet_extends({}, props, { className: className, textStyle: this.props.textStyle }),
|
|
external_React_default.a.createElement("img", { src: safeURI(props.content.icon) || DEFAULT_ICON_PATH, className: "icon" }),
|
|
external_React_default.a.createElement(
|
|
"div",
|
|
null,
|
|
this.renderTitleIcon(),
|
|
" ",
|
|
this.renderTitle(),
|
|
" ",
|
|
external_React_default.a.createElement(
|
|
"p",
|
|
{ className: "body" },
|
|
this.renderText()
|
|
),
|
|
this.props.extraContent
|
|
),
|
|
external_React_default.a.createElement(
|
|
"div",
|
|
null,
|
|
this.renderButton()
|
|
)
|
|
);
|
|
}
|
|
}
|
|
// CONCATENATED MODULE: ./content-src/asrouter/templates/EOYSnippet/EOYSnippet.jsx
|
|
var EOYSnippet_extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
|
|
|
|
|
|
|
|
|
|
class EOYSnippet_EOYSnippetBase extends external_React_default.a.PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
this.handleSubmit = this.handleSubmit.bind(this);
|
|
}
|
|
|
|
/**
|
|
* setFrequencyValue - `frequency` form parameter value should be `monthly`
|
|
* if `monthly-checkbox` is selected or `single` otherwise
|
|
*/
|
|
setFrequencyValue() {
|
|
const frequencyCheckbox = this.refs.form.querySelector("#monthly-checkbox");
|
|
if (frequencyCheckbox.checked) {
|
|
this.refs.form.querySelector("[name='frequency']").value = "monthly";
|
|
}
|
|
}
|
|
|
|
handleSubmit(event) {
|
|
event.preventDefault();
|
|
this.setFrequencyValue();
|
|
this.refs.form.submit();
|
|
if (!this.props.content.do_not_autoblock) {
|
|
this.props.onBlock();
|
|
}
|
|
}
|
|
|
|
renderDonations() {
|
|
const fieldNames = ["first", "second", "third", "fourth"];
|
|
const numberFormat = new Intl.NumberFormat(this.props.content.locale || navigator.language, {
|
|
style: "currency",
|
|
currency: this.props.content.currency_code,
|
|
minimumFractionDigits: 0
|
|
});
|
|
// Default to `second` button
|
|
const { selected_button } = this.props.content;
|
|
const btnStyle = {
|
|
color: this.props.content.button_color,
|
|
backgroundColor: this.props.content.button_background_color
|
|
};
|
|
|
|
return external_React_default.a.createElement(
|
|
"form",
|
|
{ className: "EOYSnippetForm", action: this.props.content.donation_form_url, method: this.props.form_method, onSubmit: this.handleSubmit, ref: "form" },
|
|
fieldNames.map((field, idx) => {
|
|
const button_name = `donation_amount_${field}`;
|
|
const amount = this.props.content[button_name];
|
|
return external_React_default.a.createElement(
|
|
external_React_default.a.Fragment,
|
|
{ key: idx },
|
|
external_React_default.a.createElement("input", { type: "radio", name: "amount", value: amount, id: field, defaultChecked: button_name === selected_button }),
|
|
external_React_default.a.createElement(
|
|
"label",
|
|
{ htmlFor: field, className: "donation-amount" },
|
|
numberFormat.format(amount)
|
|
)
|
|
);
|
|
}),
|
|
external_React_default.a.createElement(
|
|
"div",
|
|
{ className: "monthly-checkbox-container" },
|
|
external_React_default.a.createElement("input", { id: "monthly-checkbox", type: "checkbox" }),
|
|
external_React_default.a.createElement(
|
|
"label",
|
|
{ htmlFor: "monthly-checkbox" },
|
|
this.props.content.monthly_checkbox_label_text
|
|
)
|
|
),
|
|
external_React_default.a.createElement("input", { type: "hidden", name: "frequency", value: "single" }),
|
|
external_React_default.a.createElement("input", { type: "hidden", name: "currency", value: this.props.content.currency_code }),
|
|
external_React_default.a.createElement("input", { type: "hidden", name: "presets", value: fieldNames.map(field => this.props.content[`donation_amount_${field}`]) }),
|
|
external_React_default.a.createElement(
|
|
"button",
|
|
{ style: btnStyle, type: "submit", className: "ASRouterButton donation-form-url" },
|
|
this.props.content.button_label
|
|
)
|
|
);
|
|
}
|
|
|
|
render() {
|
|
const textStyle = {
|
|
color: this.props.content.text_color,
|
|
backgroundColor: this.props.content.background_color
|
|
};
|
|
const customElement = external_React_default.a.createElement("em", { style: { backgroundColor: this.props.content.highlight_color } });
|
|
return external_React_default.a.createElement(SimpleSnippet_SimpleSnippet, EOYSnippet_extends({}, this.props, {
|
|
className: this.props.content.test,
|
|
customElements: { em: customElement },
|
|
textStyle: textStyle,
|
|
extraContent: this.renderDonations() }));
|
|
}
|
|
}
|
|
|
|
const EOYSnippet = props => {
|
|
const extendedContent = Object.assign({
|
|
monthly_checkbox_label_text: "Make my donation monthly",
|
|
locale: "en-US",
|
|
currency_code: "usd",
|
|
selected_button: "donation_amount_second"
|
|
}, props.content);
|
|
|
|
return external_React_default.a.createElement(EOYSnippet_EOYSnippetBase, EOYSnippet_extends({}, props, {
|
|
content: extendedContent,
|
|
form_method: "GET" }));
|
|
};
|
|
// CONCATENATED MODULE: ./content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.jsx
|
|
var SubmitFormSnippet_extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
|
|
|
|
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SubmitFormSnippet_SubmitFormSnippet extends external_React_default.a.PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
this.expandSnippet = this.expandSnippet.bind(this);
|
|
this.handleSubmit = this.handleSubmit.bind(this);
|
|
this.onInputChange = this.onInputChange.bind(this);
|
|
this.state = {
|
|
expanded: false,
|
|
signupSubmitted: false,
|
|
signupSuccess: false,
|
|
disableForm: false
|
|
};
|
|
}
|
|
|
|
handleSubmit(event) {
|
|
var _this = this;
|
|
|
|
return _asyncToGenerator(function* () {
|
|
let json;
|
|
|
|
if (_this.state.disableForm) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
_this.setState({ disableForm: true });
|
|
_this.props.sendUserActionTelemetry({ event: "CLICK_BUTTON", value: "conversion-subscribe-activation", id: "NEWTAB_FOOTER_BAR_CONTENT" });
|
|
|
|
if (_this.props.form_method.toUpperCase() === "GET") {
|
|
_this.refs.form.submit();
|
|
return;
|
|
}
|
|
|
|
const { url, formData } = _this.props.processFormData ? _this.props.processFormData(_this.refs.mainInput, _this.props) : { url: _this.refs.form.action, formData: new FormData(_this.refs.form) };
|
|
|
|
try {
|
|
const fetchRequest = new Request(url, { body: formData, method: "POST" });
|
|
const response = yield fetch(fetchRequest);
|
|
json = yield response.json();
|
|
} catch (err) {
|
|
console.log(err); // eslint-disable-line no-console
|
|
}
|
|
|
|
if (json && json.status === "ok") {
|
|
_this.setState({ signupSuccess: true, signupSubmitted: true });
|
|
if (!_this.props.content.do_not_autoblock) {
|
|
_this.props.onBlock({ preventDismiss: true });
|
|
}
|
|
_this.props.sendUserActionTelemetry({ event: "CLICK_BUTTON", value: "subscribe-success", id: "NEWTAB_FOOTER_BAR_CONTENT" });
|
|
} else {
|
|
console.error("There was a problem submitting the form", json || "[No JSON response]"); // eslint-disable-line no-console
|
|
_this.setState({ signupSuccess: false, signupSubmitted: true });
|
|
_this.props.sendUserActionTelemetry({ event: "CLICK_BUTTON", value: "subscribe-error", id: "NEWTAB_FOOTER_BAR_CONTENT" });
|
|
}
|
|
|
|
_this.setState({ disableForm: false });
|
|
})();
|
|
}
|
|
|
|
expandSnippet() {
|
|
this.setState({
|
|
expanded: true,
|
|
signupSuccess: false,
|
|
signupSubmitted: false
|
|
});
|
|
}
|
|
|
|
renderHiddenFormInputs() {
|
|
const { hidden_inputs } = this.props.content;
|
|
|
|
if (!hidden_inputs) {
|
|
return null;
|
|
}
|
|
|
|
return Object.keys(hidden_inputs).map((key, idx) => external_React_default.a.createElement("input", { key: idx, type: "hidden", name: key, value: hidden_inputs[key] }));
|
|
}
|
|
|
|
renderDisclaimer() {
|
|
const { content } = this.props;
|
|
if (!content.scene2_disclaimer_html) {
|
|
return null;
|
|
}
|
|
return external_React_default.a.createElement(
|
|
"p",
|
|
{ className: "disclaimerText" },
|
|
external_React_default.a.createElement(RichText, { text: content.scene2_disclaimer_html,
|
|
localization_id: "disclaimer_html",
|
|
links: content.links,
|
|
doNotAutoBlock: true,
|
|
sendClick: this.props.sendClick })
|
|
);
|
|
}
|
|
|
|
renderFormPrivacyNotice() {
|
|
const { content } = this.props;
|
|
if (!content.scene2_privacy_html) {
|
|
return null;
|
|
}
|
|
return external_React_default.a.createElement(
|
|
"label",
|
|
{ className: "privacyNotice", htmlFor: "id_privacy" },
|
|
external_React_default.a.createElement(
|
|
"p",
|
|
null,
|
|
external_React_default.a.createElement("input", { type: "checkbox", id: "id_privacy", name: "privacy", required: "required" }),
|
|
external_React_default.a.createElement(
|
|
"span",
|
|
null,
|
|
external_React_default.a.createElement(RichText, { text: content.scene2_privacy_html,
|
|
localization_id: "privacy_html",
|
|
links: content.links,
|
|
doNotAutoBlock: true,
|
|
sendClick: this.props.sendClick })
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
renderSignupSubmitted() {
|
|
const { content } = this.props;
|
|
const isSuccess = this.state.signupSuccess;
|
|
const successTitle = isSuccess && content.success_title;
|
|
const bodyText = isSuccess ? content.success_text : content.error_text;
|
|
const retryButtonText = content.scene1_button_label;
|
|
return external_React_default.a.createElement(
|
|
SnippetBase_SnippetBase,
|
|
this.props,
|
|
external_React_default.a.createElement(
|
|
"div",
|
|
{ className: "submissionStatus" },
|
|
successTitle ? external_React_default.a.createElement(
|
|
"h2",
|
|
{ className: "submitStatusTitle" },
|
|
successTitle
|
|
) : null,
|
|
external_React_default.a.createElement(
|
|
"p",
|
|
null,
|
|
bodyText,
|
|
isSuccess ? null : external_React_default.a.createElement(
|
|
Button,
|
|
{ onClick: this.expandSnippet },
|
|
retryButtonText
|
|
)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
onInputChange(event) {
|
|
if (!this.props.validateInput) {
|
|
return;
|
|
}
|
|
const hasError = this.props.validateInput(event.target.value, this.props.content);
|
|
event.target.setCustomValidity(hasError);
|
|
}
|
|
|
|
renderInput() {
|
|
const placholder = this.props.content.scene2_email_placeholder_text || this.props.content.scene2_input_placeholder;
|
|
return external_React_default.a.createElement("input", {
|
|
ref: "mainInput",
|
|
type: this.props.inputType || "email",
|
|
className: "mainInput",
|
|
name: "email",
|
|
required: true,
|
|
placeholder: placholder,
|
|
onChange: this.props.validateInput ? this.onInputChange : null,
|
|
autoFocus: true });
|
|
}
|
|
|
|
renderSignupView() {
|
|
const { content } = this.props;
|
|
const containerClass = `SubmitFormSnippet ${this.props.className}`;
|
|
return external_React_default.a.createElement(
|
|
SnippetBase_SnippetBase,
|
|
SubmitFormSnippet_extends({}, this.props, { className: containerClass, footerDismiss: true }),
|
|
content.scene2_icon ? external_React_default.a.createElement(
|
|
"div",
|
|
{ className: "scene2Icon" },
|
|
external_React_default.a.createElement("img", { src: content.scene2_icon })
|
|
) : null,
|
|
external_React_default.a.createElement(
|
|
"div",
|
|
{ className: "message" },
|
|
external_React_default.a.createElement(
|
|
"p",
|
|
null,
|
|
content.scene2_text
|
|
)
|
|
),
|
|
external_React_default.a.createElement(
|
|
"form",
|
|
{ action: content.form_action, method: this.props.form_method, onSubmit: this.handleSubmit, ref: "form" },
|
|
this.renderHiddenFormInputs(),
|
|
external_React_default.a.createElement(
|
|
"div",
|
|
null,
|
|
this.renderInput(),
|
|
external_React_default.a.createElement(
|
|
"button",
|
|
{ type: "submit", className: "ASRouterButton primary", ref: "formSubmitBtn" },
|
|
content.scene2_button_label
|
|
)
|
|
),
|
|
this.renderFormPrivacyNotice() || this.renderDisclaimer()
|
|
)
|
|
);
|
|
}
|
|
|
|
getFirstSceneContent() {
|
|
return Object.keys(this.props.content).filter(key => key.includes("scene1")).reduce((acc, key) => {
|
|
acc[key.substr(7)] = this.props.content[key];
|
|
return acc;
|
|
}, {});
|
|
}
|
|
|
|
render() {
|
|
const content = Object.assign({}, this.props.content, this.getFirstSceneContent());
|
|
|
|
if (this.state.signupSubmitted) {
|
|
return this.renderSignupSubmitted();
|
|
}
|
|
if (this.state.expanded) {
|
|
return this.renderSignupView();
|
|
}
|
|
return external_React_default.a.createElement(SimpleSnippet_SimpleSnippet, SubmitFormSnippet_extends({}, this.props, { content: content, onButtonClick: this.expandSnippet }));
|
|
}
|
|
}
|
|
// CONCATENATED MODULE: ./content-src/asrouter/templates/FXASignupSnippet/FXASignupSnippet.jsx
|
|
var FXASignupSnippet_extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
|
|
|
|
|
|
|
|
|
|
const FXASignupSnippet = props => {
|
|
const userAgent = window.navigator.userAgent.match(/Firefox\/([0-9]+)\./);
|
|
const firefox_version = userAgent ? parseInt(userAgent[1], 10) : 0;
|
|
const extendedContent = Object.assign({
|
|
form_action: "https://accounts.firefox.com/"
|
|
}, props.content, {
|
|
hidden_inputs: Object.assign({
|
|
action: "email",
|
|
context: "fx_desktop_v3",
|
|
entrypoint: "snippets",
|
|
service: "sync",
|
|
utm_source: "snippet",
|
|
utm_content: firefox_version,
|
|
utm_campaign: props.content.utm_campaign,
|
|
utm_term: props.content.utm_term
|
|
}, props.content.hidden_inputs)
|
|
});
|
|
|
|
return external_React_default.a.createElement(SubmitFormSnippet_SubmitFormSnippet, FXASignupSnippet_extends({}, props, {
|
|
content: extendedContent,
|
|
form_method: "GET" }));
|
|
};
|
|
// CONCATENATED MODULE: ./content-src/asrouter/templates/NewsletterSnippet/NewsletterSnippet.jsx
|
|
var NewsletterSnippet_extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
|
|
|
|
|
|
|
|
|
|
const NewsletterSnippet = props => {
|
|
const extendedContent = Object.assign({
|
|
form_action: "https://basket.mozilla.org/subscribe.json"
|
|
}, props.content, {
|
|
hidden_inputs: Object.assign({
|
|
newsletters: props.content.scene2_newsletter || "mozilla-foundation",
|
|
fmt: "H",
|
|
lang: "en-US",
|
|
source_url: `https://snippets.mozilla.com/show/${props.id}`
|
|
}, props.content.hidden_inputs)
|
|
});
|
|
|
|
return external_React_default.a.createElement(SubmitFormSnippet_SubmitFormSnippet, NewsletterSnippet_extends({}, props, {
|
|
content: extendedContent,
|
|
form_method: "POST" }));
|
|
};
|
|
// CONCATENATED MODULE: ./content-src/asrouter/templates/SendToDeviceSnippet/isEmailOrPhoneNumber.js
|
|
/**
|
|
* Checks if a given string is an email or phone number or neither
|
|
* @param {string} val The user input
|
|
* @param {ASRMessageContent} content .content property on ASR message
|
|
* @returns {"email"|"phone"|""} The type of the input
|
|
*/
|
|
function isEmailOrPhoneNumber(val, content) {
|
|
const { locale } = content;
|
|
// http://emailregex.com/
|
|
const email_re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
|
const check_email = email_re.test(val);
|
|
let check_phone; // depends on locale
|
|
switch (locale) {
|
|
case "en-US":
|
|
case "en-CA":
|
|
// allow 10-11 digits in case user wants to enter country code
|
|
check_phone = val.length >= 10 && val.length <= 11 && !isNaN(val);
|
|
break;
|
|
case "de":
|
|
// allow between 2 and 12 digits for german phone numbers
|
|
check_phone = val.length >= 2 && val.length <= 12 && !isNaN(val);
|
|
break;
|
|
// this case should never be hit, but good to have a fallback just in case
|
|
default:
|
|
check_phone = !isNaN(val);
|
|
break;
|
|
}
|
|
if (check_email) {
|
|
return "email";
|
|
} else if (check_phone) {
|
|
return "phone";
|
|
}
|
|
return "";
|
|
}
|
|
// CONCATENATED MODULE: ./content-src/asrouter/templates/SendToDeviceSnippet/SendToDeviceSnippet.jsx
|
|
var SendToDeviceSnippet_extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
|
|
|
|
|
|
|
|
|
|
|
|
function validateInput(value, content) {
|
|
const type = isEmailOrPhoneNumber(value, content);
|
|
return type ? "" : "Must be an email or a phone number.";
|
|
}
|
|
|
|
function processFormData(input, message) {
|
|
const { content } = message;
|
|
const type = content.include_sms ? isEmailOrPhoneNumber(input.value, content) : "email";
|
|
const formData = new FormData();
|
|
let url;
|
|
if (type === "phone") {
|
|
url = "https://basket.mozilla.org/news/subscribe_sms/";
|
|
formData.append("mobile_number", input.value);
|
|
formData.append("msg_name", content.message_id_sms);
|
|
formData.append("country", content.country);
|
|
} else if (type === "email") {
|
|
url = "https://basket.mozilla.org/news/subscribe/";
|
|
formData.append("email", input.value);
|
|
formData.append("newsletters", content.message_id_email);
|
|
formData.append("source_url", encodeURIComponent(`https://snippets.mozilla.com/show/${message.id}`));
|
|
}
|
|
formData.append("lang", content.locale);
|
|
return { formData, url };
|
|
}
|
|
|
|
const SendToDeviceSnippet = props => external_React_default.a.createElement(SubmitFormSnippet_SubmitFormSnippet, SendToDeviceSnippet_extends({}, props, {
|
|
form_method: "POST",
|
|
className: "send_to_device_snippet",
|
|
inputType: props.content.include_sms ? "text" : "email",
|
|
validateInput: props.content.include_sms ? validateInput : null,
|
|
processFormData: processFormData }));
|
|
// CONCATENATED MODULE: ./content-src/asrouter/templates/template-manifest.jsx
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SnippetsTemplates", function() { return SnippetsTemplates; });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Key names matching schema name of templates
|
|
const SnippetsTemplates = {
|
|
simple_snippet: SimpleSnippet_SimpleSnippet,
|
|
newsletter_snippet: NewsletterSnippet,
|
|
fxa_signup_snippet: FXASignupSnippet,
|
|
send_to_device_snippet: SendToDeviceSnippet,
|
|
eoy_snippet: EOYSnippet
|
|
};
|
|
|
|
/***/ }),
|
|
/* 43 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
|
|
// CONCATENATED MODULE: ./node_modules/fluent/src/parser.js
|
|
/* eslint no-magic-numbers: [0] */
|
|
|
|
const MAX_PLACEABLES = 100;
|
|
|
|
const entryIdentifierRe = /-?[a-zA-Z][a-zA-Z0-9_-]*/y;
|
|
const identifierRe = /[a-zA-Z][a-zA-Z0-9_-]*/y;
|
|
const functionIdentifierRe = /^[A-Z][A-Z_?-]*$/;
|
|
|
|
/**
|
|
* The `Parser` class is responsible for parsing FTL resources.
|
|
*
|
|
* It's only public method is `getResource(source)` which takes an FTL string
|
|
* and returns a two element Array with an Object of entries generated from the
|
|
* source as the first element and an array of SyntaxError objects as the
|
|
* second.
|
|
*
|
|
* This parser is optimized for runtime performance.
|
|
*
|
|
* There is an equivalent of this parser in syntax/parser which is
|
|
* generating full AST which is useful for FTL tools.
|
|
*/
|
|
class RuntimeParser {
|
|
/**
|
|
* Parse FTL code into entries formattable by the MessageContext.
|
|
*
|
|
* Given a string of FTL syntax, return a map of entries that can be passed
|
|
* to MessageContext.format and a list of errors encountered during parsing.
|
|
*
|
|
* @param {String} string
|
|
* @returns {Array<Object, Array>}
|
|
*/
|
|
getResource(string) {
|
|
this._source = string;
|
|
this._index = 0;
|
|
this._length = string.length;
|
|
this.entries = {};
|
|
|
|
const errors = [];
|
|
|
|
this.skipWS();
|
|
while (this._index < this._length) {
|
|
try {
|
|
this.getEntry();
|
|
} catch (e) {
|
|
if (e instanceof SyntaxError) {
|
|
errors.push(e);
|
|
|
|
this.skipToNextEntryStart();
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
this.skipWS();
|
|
}
|
|
|
|
return [this.entries, errors];
|
|
}
|
|
|
|
/**
|
|
* Parse the source string from the current index as an FTL entry
|
|
* and add it to object's entries property.
|
|
*
|
|
* @private
|
|
*/
|
|
getEntry() {
|
|
// The index here should either be at the beginning of the file
|
|
// or right after new line.
|
|
if (this._index !== 0 && this._source[this._index - 1] !== "\n") {
|
|
throw this.error(`Expected an entry to start
|
|
at the beginning of the file or on a new line.`);
|
|
}
|
|
|
|
const ch = this._source[this._index];
|
|
|
|
// We don't care about comments or sections at runtime
|
|
if (ch === "/" || ch === "#" && [" ", "#", "\n"].includes(this._source[this._index + 1])) {
|
|
this.skipComment();
|
|
return;
|
|
}
|
|
|
|
if (ch === "[") {
|
|
this.skipSection();
|
|
return;
|
|
}
|
|
|
|
this.getMessage();
|
|
}
|
|
|
|
/**
|
|
* Skip the section entry from the current index.
|
|
*
|
|
* @private
|
|
*/
|
|
skipSection() {
|
|
this._index += 1;
|
|
if (this._source[this._index] !== "[") {
|
|
throw this.error('Expected "[[" to open a section');
|
|
}
|
|
|
|
this._index += 1;
|
|
|
|
this.skipInlineWS();
|
|
this.getVariantName();
|
|
this.skipInlineWS();
|
|
|
|
if (this._source[this._index] !== "]" || this._source[this._index + 1] !== "]") {
|
|
throw this.error('Expected "]]" to close a section');
|
|
}
|
|
|
|
this._index += 2;
|
|
}
|
|
|
|
/**
|
|
* Parse the source string from the current index as an FTL message
|
|
* and add it to the entries property on the Parser.
|
|
*
|
|
* @private
|
|
*/
|
|
getMessage() {
|
|
const id = this.getEntryIdentifier();
|
|
|
|
this.skipInlineWS();
|
|
|
|
if (this._source[this._index] === "=") {
|
|
this._index++;
|
|
}
|
|
|
|
this.skipInlineWS();
|
|
|
|
const val = this.getPattern();
|
|
|
|
if (id.startsWith("-") && val === null) {
|
|
throw this.error("Expected term to have a value");
|
|
}
|
|
|
|
let attrs = null;
|
|
|
|
if (this._source[this._index] === " ") {
|
|
const lineStart = this._index;
|
|
this.skipInlineWS();
|
|
|
|
if (this._source[this._index] === ".") {
|
|
this._index = lineStart;
|
|
attrs = this.getAttributes();
|
|
}
|
|
}
|
|
|
|
if (attrs === null && typeof val === "string") {
|
|
this.entries[id] = val;
|
|
} else {
|
|
if (val === null && attrs === null) {
|
|
throw this.error("Expected message to have a value or attributes");
|
|
}
|
|
|
|
this.entries[id] = {};
|
|
|
|
if (val !== null) {
|
|
this.entries[id].val = val;
|
|
}
|
|
|
|
if (attrs !== null) {
|
|
this.entries[id].attrs = attrs;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Skip whitespace.
|
|
*
|
|
* @private
|
|
*/
|
|
skipWS() {
|
|
let ch = this._source[this._index];
|
|
while (ch === " " || ch === "\n" || ch === "\t" || ch === "\r") {
|
|
ch = this._source[++this._index];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Skip inline whitespace (space and \t).
|
|
*
|
|
* @private
|
|
*/
|
|
skipInlineWS() {
|
|
let ch = this._source[this._index];
|
|
while (ch === " " || ch === "\t") {
|
|
ch = this._source[++this._index];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Skip blank lines.
|
|
*
|
|
* @private
|
|
*/
|
|
skipBlankLines() {
|
|
while (true) {
|
|
const ptr = this._index;
|
|
|
|
this.skipInlineWS();
|
|
|
|
if (this._source[this._index] === "\n") {
|
|
this._index += 1;
|
|
} else {
|
|
this._index = ptr;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get identifier using the provided regex.
|
|
*
|
|
* By default this will get identifiers of public messages, attributes and
|
|
* external arguments (without the $).
|
|
*
|
|
* @returns {String}
|
|
* @private
|
|
*/
|
|
getIdentifier(re = identifierRe) {
|
|
re.lastIndex = this._index;
|
|
const result = re.exec(this._source);
|
|
|
|
if (result === null) {
|
|
this._index += 1;
|
|
throw this.error(`Expected an identifier [${re.toString()}]`);
|
|
}
|
|
|
|
this._index = re.lastIndex;
|
|
return result[0];
|
|
}
|
|
|
|
/**
|
|
* Get identifier of a Message or a Term (staring with a dash).
|
|
*
|
|
* @returns {String}
|
|
* @private
|
|
*/
|
|
getEntryIdentifier() {
|
|
return this.getIdentifier(entryIdentifierRe);
|
|
}
|
|
|
|
/**
|
|
* Get Variant name.
|
|
*
|
|
* @returns {Object}
|
|
* @private
|
|
*/
|
|
getVariantName() {
|
|
let name = "";
|
|
|
|
const start = this._index;
|
|
let cc = this._source.charCodeAt(this._index);
|
|
|
|
if (cc >= 97 && cc <= 122 || // a-z
|
|
cc >= 65 && cc <= 90 || // A-Z
|
|
cc === 95 || cc === 32) {
|
|
// _ <space>
|
|
cc = this._source.charCodeAt(++this._index);
|
|
} else {
|
|
throw this.error("Expected a keyword (starting with [a-zA-Z_])");
|
|
}
|
|
|
|
while (cc >= 97 && cc <= 122 || // a-z
|
|
cc >= 65 && cc <= 90 || // A-Z
|
|
cc >= 48 && cc <= 57 || // 0-9
|
|
cc === 95 || cc === 45 || cc === 32) {
|
|
// _- <space>
|
|
cc = this._source.charCodeAt(++this._index);
|
|
}
|
|
|
|
// If we encountered the end of name, we want to test if the last
|
|
// collected character is a space.
|
|
// If it is, we will backtrack to the last non-space character because
|
|
// the keyword cannot end with a space character.
|
|
while (this._source.charCodeAt(this._index - 1) === 32) {
|
|
this._index--;
|
|
}
|
|
|
|
name += this._source.slice(start, this._index);
|
|
|
|
return { type: "varname", name };
|
|
}
|
|
|
|
/**
|
|
* Get simple string argument enclosed in `"`.
|
|
*
|
|
* @returns {String}
|
|
* @private
|
|
*/
|
|
getString() {
|
|
const start = this._index + 1;
|
|
|
|
while (++this._index < this._length) {
|
|
const ch = this._source[this._index];
|
|
|
|
if (ch === '"') {
|
|
break;
|
|
}
|
|
|
|
if (ch === "\n") {
|
|
throw this.error("Unterminated string expression");
|
|
}
|
|
}
|
|
|
|
return this._source.substring(start, this._index++);
|
|
}
|
|
|
|
/**
|
|
* Parses a Message pattern.
|
|
* Message Pattern may be a simple string or an array of strings
|
|
* and placeable expressions.
|
|
*
|
|
* @returns {String|Array}
|
|
* @private
|
|
*/
|
|
getPattern() {
|
|
// We're going to first try to see if the pattern is simple.
|
|
// If it is we can just look for the end of the line and read the string.
|
|
//
|
|
// Then, if either the line contains a placeable opening `{` or the
|
|
// next line starts an indentation, we switch to complex pattern.
|
|
const start = this._index;
|
|
let eol = this._source.indexOf("\n", this._index);
|
|
|
|
if (eol === -1) {
|
|
eol = this._length;
|
|
}
|
|
|
|
const firstLineContent = start !== eol ? this._source.slice(start, eol) : null;
|
|
|
|
if (firstLineContent && firstLineContent.includes("{")) {
|
|
return this.getComplexPattern();
|
|
}
|
|
|
|
this._index = eol + 1;
|
|
|
|
this.skipBlankLines();
|
|
|
|
if (this._source[this._index] !== " ") {
|
|
// No indentation means we're done with this message. Callers should check
|
|
// if the return value here is null. It may be OK for messages, but not OK
|
|
// for terms, attributes and variants.
|
|
return firstLineContent;
|
|
}
|
|
|
|
const lineStart = this._index;
|
|
|
|
this.skipInlineWS();
|
|
|
|
if (this._source[this._index] === ".") {
|
|
// The pattern is followed by an attribute. Rewind _index to the first
|
|
// column of the current line as expected by getAttributes.
|
|
this._index = lineStart;
|
|
return firstLineContent;
|
|
}
|
|
|
|
if (firstLineContent) {
|
|
// It's a multiline pattern which started on the same line as the
|
|
// identifier. Reparse the whole pattern to make sure we get all of it.
|
|
this._index = start;
|
|
}
|
|
|
|
return this.getComplexPattern();
|
|
}
|
|
|
|
/**
|
|
* Parses a complex Message pattern.
|
|
* This function is called by getPattern when the message is multiline,
|
|
* or contains escape chars or placeables.
|
|
* It does full parsing of complex patterns.
|
|
*
|
|
* @returns {Array}
|
|
* @private
|
|
*/
|
|
/* eslint-disable complexity */
|
|
getComplexPattern() {
|
|
let buffer = "";
|
|
const content = [];
|
|
let placeables = 0;
|
|
|
|
let ch = this._source[this._index];
|
|
|
|
while (this._index < this._length) {
|
|
// This block handles multi-line strings combining strings separated
|
|
// by new line.
|
|
if (ch === "\n") {
|
|
this._index++;
|
|
|
|
// We want to capture the start and end pointers
|
|
// around blank lines and add them to the buffer
|
|
// but only if the blank lines are in the middle
|
|
// of the string.
|
|
const blankLinesStart = this._index;
|
|
this.skipBlankLines();
|
|
const blankLinesEnd = this._index;
|
|
|
|
if (this._source[this._index] !== " ") {
|
|
break;
|
|
}
|
|
this.skipInlineWS();
|
|
|
|
if (this._source[this._index] === "}" || this._source[this._index] === "[" || this._source[this._index] === "*" || this._source[this._index] === ".") {
|
|
this._index = blankLinesEnd;
|
|
break;
|
|
}
|
|
|
|
buffer += this._source.substring(blankLinesStart, blankLinesEnd);
|
|
|
|
if (buffer.length || content.length) {
|
|
buffer += "\n";
|
|
}
|
|
ch = this._source[this._index];
|
|
continue;
|
|
} else if (ch === "\\") {
|
|
const ch2 = this._source[this._index + 1];
|
|
if (ch2 === '"' || ch2 === "{" || ch2 === "\\") {
|
|
ch = ch2;
|
|
this._index++;
|
|
}
|
|
} else if (ch === "{") {
|
|
// Push the buffer to content array right before placeable
|
|
if (buffer.length) {
|
|
content.push(buffer);
|
|
}
|
|
if (placeables > MAX_PLACEABLES - 1) {
|
|
throw this.error(`Too many placeables, maximum allowed is ${MAX_PLACEABLES}`);
|
|
}
|
|
buffer = "";
|
|
content.push(this.getPlaceable());
|
|
|
|
this._index++;
|
|
|
|
ch = this._source[this._index];
|
|
placeables++;
|
|
continue;
|
|
}
|
|
|
|
if (ch) {
|
|
buffer += ch;
|
|
}
|
|
this._index++;
|
|
ch = this._source[this._index];
|
|
}
|
|
|
|
if (content.length === 0) {
|
|
return buffer.length ? buffer : null;
|
|
}
|
|
|
|
if (buffer.length) {
|
|
content.push(buffer);
|
|
}
|
|
|
|
return content;
|
|
}
|
|
/* eslint-enable complexity */
|
|
|
|
/**
|
|
* Parses a single placeable in a Message pattern and returns its
|
|
* expression.
|
|
*
|
|
* @returns {Object}
|
|
* @private
|
|
*/
|
|
getPlaceable() {
|
|
const start = ++this._index;
|
|
|
|
this.skipWS();
|
|
|
|
if (this._source[this._index] === "*" || this._source[this._index] === "[" && this._source[this._index + 1] !== "]") {
|
|
const variants = this.getVariants();
|
|
|
|
return {
|
|
type: "sel",
|
|
exp: null,
|
|
vars: variants[0],
|
|
def: variants[1]
|
|
};
|
|
}
|
|
|
|
// Rewind the index and only support in-line white-space now.
|
|
this._index = start;
|
|
this.skipInlineWS();
|
|
|
|
const selector = this.getSelectorExpression();
|
|
|
|
this.skipWS();
|
|
|
|
const ch = this._source[this._index];
|
|
|
|
if (ch === "}") {
|
|
if (selector.type === "attr" && selector.id.name.startsWith("-")) {
|
|
throw this.error("Attributes of private messages cannot be interpolated.");
|
|
}
|
|
|
|
return selector;
|
|
}
|
|
|
|
if (ch !== "-" || this._source[this._index + 1] !== ">") {
|
|
throw this.error('Expected "}" or "->"');
|
|
}
|
|
|
|
if (selector.type === "ref") {
|
|
throw this.error("Message references cannot be used as selectors.");
|
|
}
|
|
|
|
if (selector.type === "var") {
|
|
throw this.error("Variants cannot be used as selectors.");
|
|
}
|
|
|
|
if (selector.type === "attr" && !selector.id.name.startsWith("-")) {
|
|
throw this.error("Attributes of public messages cannot be used as selectors.");
|
|
}
|
|
|
|
this._index += 2; // ->
|
|
|
|
this.skipInlineWS();
|
|
|
|
if (this._source[this._index] !== "\n") {
|
|
throw this.error("Variants should be listed in a new line");
|
|
}
|
|
|
|
this.skipWS();
|
|
|
|
const variants = this.getVariants();
|
|
|
|
if (variants[0].length === 0) {
|
|
throw this.error("Expected members for the select expression");
|
|
}
|
|
|
|
return {
|
|
type: "sel",
|
|
exp: selector,
|
|
vars: variants[0],
|
|
def: variants[1]
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Parses a selector expression.
|
|
*
|
|
* @returns {Object}
|
|
* @private
|
|
*/
|
|
getSelectorExpression() {
|
|
const literal = this.getLiteral();
|
|
|
|
if (literal.type !== "ref") {
|
|
return literal;
|
|
}
|
|
|
|
if (this._source[this._index] === ".") {
|
|
this._index++;
|
|
|
|
const name = this.getIdentifier();
|
|
this._index++;
|
|
return {
|
|
type: "attr",
|
|
id: literal,
|
|
name
|
|
};
|
|
}
|
|
|
|
if (this._source[this._index] === "[") {
|
|
this._index++;
|
|
|
|
const key = this.getVariantKey();
|
|
this._index++;
|
|
return {
|
|
type: "var",
|
|
id: literal,
|
|
key
|
|
};
|
|
}
|
|
|
|
if (this._source[this._index] === "(") {
|
|
this._index++;
|
|
const args = this.getCallArgs();
|
|
|
|
if (!functionIdentifierRe.test(literal.name)) {
|
|
throw this.error("Function names must be all upper-case");
|
|
}
|
|
|
|
this._index++;
|
|
|
|
literal.type = "fun";
|
|
|
|
return {
|
|
type: "call",
|
|
fun: literal,
|
|
args
|
|
};
|
|
}
|
|
|
|
return literal;
|
|
}
|
|
|
|
/**
|
|
* Parses call arguments for a CallExpression.
|
|
*
|
|
* @returns {Array}
|
|
* @private
|
|
*/
|
|
getCallArgs() {
|
|
const args = [];
|
|
|
|
while (this._index < this._length) {
|
|
this.skipInlineWS();
|
|
|
|
if (this._source[this._index] === ")") {
|
|
return args;
|
|
}
|
|
|
|
const exp = this.getSelectorExpression();
|
|
|
|
// MessageReference in this place may be an entity reference, like:
|
|
// `call(foo)`, or, if it's followed by `:` it will be a key-value pair.
|
|
if (exp.type !== "ref") {
|
|
args.push(exp);
|
|
} else {
|
|
this.skipInlineWS();
|
|
|
|
if (this._source[this._index] === ":") {
|
|
this._index++;
|
|
this.skipInlineWS();
|
|
|
|
const val = this.getSelectorExpression();
|
|
|
|
// If the expression returned as a value of the argument
|
|
// is not a quote delimited string or number, throw.
|
|
//
|
|
// We don't have to check here if the pattern is quote delimited
|
|
// because that's the only type of string allowed in expressions.
|
|
if (typeof val === "string" || Array.isArray(val) || val.type === "num") {
|
|
args.push({
|
|
type: "narg",
|
|
name: exp.name,
|
|
val
|
|
});
|
|
} else {
|
|
this._index = this._source.lastIndexOf(":", this._index) + 1;
|
|
throw this.error("Expected string in quotes, number.");
|
|
}
|
|
} else {
|
|
args.push(exp);
|
|
}
|
|
}
|
|
|
|
this.skipInlineWS();
|
|
|
|
if (this._source[this._index] === ")") {
|
|
break;
|
|
} else if (this._source[this._index] === ",") {
|
|
this._index++;
|
|
} else {
|
|
throw this.error('Expected "," or ")"');
|
|
}
|
|
}
|
|
|
|
return args;
|
|
}
|
|
|
|
/**
|
|
* Parses an FTL Number.
|
|
*
|
|
* @returns {Object}
|
|
* @private
|
|
*/
|
|
getNumber() {
|
|
let num = "";
|
|
let cc = this._source.charCodeAt(this._index);
|
|
|
|
// The number literal may start with negative sign `-`.
|
|
if (cc === 45) {
|
|
num += "-";
|
|
cc = this._source.charCodeAt(++this._index);
|
|
}
|
|
|
|
// next, we expect at least one digit
|
|
if (cc < 48 || cc > 57) {
|
|
throw this.error(`Unknown literal "${num}"`);
|
|
}
|
|
|
|
// followed by potentially more digits
|
|
while (cc >= 48 && cc <= 57) {
|
|
num += this._source[this._index++];
|
|
cc = this._source.charCodeAt(this._index);
|
|
}
|
|
|
|
// followed by an optional decimal separator `.`
|
|
if (cc === 46) {
|
|
num += this._source[this._index++];
|
|
cc = this._source.charCodeAt(this._index);
|
|
|
|
// followed by at least one digit
|
|
if (cc < 48 || cc > 57) {
|
|
throw this.error(`Unknown literal "${num}"`);
|
|
}
|
|
|
|
// and optionally more digits
|
|
while (cc >= 48 && cc <= 57) {
|
|
num += this._source[this._index++];
|
|
cc = this._source.charCodeAt(this._index);
|
|
}
|
|
}
|
|
|
|
return {
|
|
type: "num",
|
|
val: num
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Parses a list of Message attributes.
|
|
*
|
|
* @returns {Object}
|
|
* @private
|
|
*/
|
|
getAttributes() {
|
|
const attrs = {};
|
|
|
|
while (this._index < this._length) {
|
|
if (this._source[this._index] !== " ") {
|
|
break;
|
|
}
|
|
this.skipInlineWS();
|
|
|
|
if (this._source[this._index] !== ".") {
|
|
break;
|
|
}
|
|
this._index++;
|
|
|
|
const key = this.getIdentifier();
|
|
|
|
this.skipInlineWS();
|
|
|
|
if (this._source[this._index] !== "=") {
|
|
throw this.error('Expected "="');
|
|
}
|
|
this._index++;
|
|
|
|
this.skipInlineWS();
|
|
|
|
const val = this.getPattern();
|
|
|
|
if (val === null) {
|
|
throw this.error("Expected attribute to have a value");
|
|
}
|
|
|
|
if (typeof val === "string") {
|
|
attrs[key] = val;
|
|
} else {
|
|
attrs[key] = {
|
|
val
|
|
};
|
|
}
|
|
|
|
this.skipBlankLines();
|
|
}
|
|
|
|
return attrs;
|
|
}
|
|
|
|
/**
|
|
* Parses a list of Selector variants.
|
|
*
|
|
* @returns {Array}
|
|
* @private
|
|
*/
|
|
getVariants() {
|
|
const variants = [];
|
|
let index = 0;
|
|
let defaultIndex;
|
|
|
|
while (this._index < this._length) {
|
|
const ch = this._source[this._index];
|
|
|
|
if ((ch !== "[" || this._source[this._index + 1] === "[") && ch !== "*") {
|
|
break;
|
|
}
|
|
if (ch === "*") {
|
|
this._index++;
|
|
defaultIndex = index;
|
|
}
|
|
|
|
if (this._source[this._index] !== "[") {
|
|
throw this.error('Expected "["');
|
|
}
|
|
|
|
this._index++;
|
|
|
|
const key = this.getVariantKey();
|
|
|
|
this.skipInlineWS();
|
|
|
|
const val = this.getPattern();
|
|
|
|
if (val === null) {
|
|
throw this.error("Expected variant to have a value");
|
|
}
|
|
|
|
variants[index++] = { key, val };
|
|
|
|
this.skipWS();
|
|
}
|
|
|
|
return [variants, defaultIndex];
|
|
}
|
|
|
|
/**
|
|
* Parses a Variant key.
|
|
*
|
|
* @returns {String}
|
|
* @private
|
|
*/
|
|
getVariantKey() {
|
|
// VariantKey may be a Keyword or Number
|
|
|
|
const cc = this._source.charCodeAt(this._index);
|
|
let literal;
|
|
|
|
if (cc >= 48 && cc <= 57 || cc === 45) {
|
|
literal = this.getNumber();
|
|
} else {
|
|
literal = this.getVariantName();
|
|
}
|
|
|
|
if (this._source[this._index] !== "]") {
|
|
throw this.error('Expected "]"');
|
|
}
|
|
|
|
this._index++;
|
|
return literal;
|
|
}
|
|
|
|
/**
|
|
* Parses an FTL literal.
|
|
*
|
|
* @returns {Object}
|
|
* @private
|
|
*/
|
|
getLiteral() {
|
|
const cc0 = this._source.charCodeAt(this._index);
|
|
|
|
if (cc0 === 36) {
|
|
// $
|
|
this._index++;
|
|
return {
|
|
type: "ext",
|
|
name: this.getIdentifier()
|
|
};
|
|
}
|
|
|
|
const cc1 = cc0 === 45 // -
|
|
// Peek at the next character after the dash.
|
|
? this._source.charCodeAt(this._index + 1)
|
|
// Or keep using the character at the current index.
|
|
: cc0;
|
|
|
|
if (cc1 >= 97 && cc1 <= 122 || // a-z
|
|
cc1 >= 65 && cc1 <= 90) {
|
|
// A-Z
|
|
return {
|
|
type: "ref",
|
|
name: this.getEntryIdentifier()
|
|
};
|
|
}
|
|
|
|
if (cc1 >= 48 && cc1 <= 57) {
|
|
// 0-9
|
|
return this.getNumber();
|
|
}
|
|
|
|
if (cc0 === 34) {
|
|
// "
|
|
return this.getString();
|
|
}
|
|
|
|
throw this.error("Expected literal");
|
|
}
|
|
|
|
/**
|
|
* Skips an FTL comment.
|
|
*
|
|
* @private
|
|
*/
|
|
skipComment() {
|
|
// At runtime, we don't care about comments so we just have
|
|
// to parse them properly and skip their content.
|
|
let eol = this._source.indexOf("\n", this._index);
|
|
|
|
while (eol !== -1 && (this._source[eol + 1] === "/" && this._source[eol + 2] === "/" || this._source[eol + 1] === "#" && [" ", "#"].includes(this._source[eol + 2]))) {
|
|
this._index = eol + 3;
|
|
|
|
eol = this._source.indexOf("\n", this._index);
|
|
|
|
if (eol === -1) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (eol === -1) {
|
|
this._index = this._length;
|
|
} else {
|
|
this._index = eol + 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a new SyntaxError object with a given message.
|
|
*
|
|
* @param {String} message
|
|
* @returns {Object}
|
|
* @private
|
|
*/
|
|
error(message) {
|
|
return new SyntaxError(message);
|
|
}
|
|
|
|
/**
|
|
* Skips to the beginning of a next entry after the current position.
|
|
* This is used to mark the boundary of junk entry in case of error,
|
|
* and recover from the returned position.
|
|
*
|
|
* @private
|
|
*/
|
|
skipToNextEntryStart() {
|
|
let start = this._index;
|
|
|
|
while (true) {
|
|
if (start === 0 || this._source[start - 1] === "\n") {
|
|
const cc = this._source.charCodeAt(start);
|
|
|
|
if (cc >= 97 && cc <= 122 || // a-z
|
|
cc >= 65 && cc <= 90 || // A-Z
|
|
cc === 47 || cc === 91) {
|
|
// /[
|
|
this._index = start;
|
|
return;
|
|
}
|
|
}
|
|
|
|
start = this._source.indexOf("\n", start);
|
|
|
|
if (start === -1) {
|
|
this._index = this._length;
|
|
return;
|
|
}
|
|
start++;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses an FTL string using RuntimeParser and returns the generated
|
|
* object with entries and a list of errors.
|
|
*
|
|
* @param {String} string
|
|
* @returns {Array<Object, Array>}
|
|
*/
|
|
function parse(string) {
|
|
const parser = new RuntimeParser();
|
|
return parser.getResource(string);
|
|
}
|
|
// CONCATENATED MODULE: ./node_modules/fluent/src/types.js
|
|
/* global Intl */
|
|
|
|
/**
|
|
* The `FluentType` class is the base of Fluent's type system.
|
|
*
|
|
* Fluent types wrap JavaScript values and store additional configuration for
|
|
* them, which can then be used in the `toString` method together with a proper
|
|
* `Intl` formatter.
|
|
*/
|
|
class FluentType {
|
|
|
|
/**
|
|
* Create an `FluentType` instance.
|
|
*
|
|
* @param {Any} value - JavaScript value to wrap.
|
|
* @param {Object} opts - Configuration.
|
|
* @returns {FluentType}
|
|
*/
|
|
constructor(value, opts) {
|
|
this.value = value;
|
|
this.opts = opts;
|
|
}
|
|
|
|
/**
|
|
* Unwrap the raw value stored by this `FluentType`.
|
|
*
|
|
* @returns {Any}
|
|
*/
|
|
valueOf() {
|
|
return this.value;
|
|
}
|
|
|
|
/**
|
|
* Format this instance of `FluentType` to a string.
|
|
*
|
|
* Formatted values are suitable for use outside of the `MessageContext`.
|
|
* This method can use `Intl` formatters memoized by the `MessageContext`
|
|
* instance passed as an argument.
|
|
*
|
|
* @param {MessageContext} [ctx]
|
|
* @returns {string}
|
|
*/
|
|
toString() {
|
|
throw new Error("Subclasses of FluentType must implement toString.");
|
|
}
|
|
}
|
|
|
|
class FluentNone extends FluentType {
|
|
toString() {
|
|
return this.value || "???";
|
|
}
|
|
}
|
|
|
|
class FluentNumber extends FluentType {
|
|
constructor(value, opts) {
|
|
super(parseFloat(value), opts);
|
|
}
|
|
|
|
toString(ctx) {
|
|
try {
|
|
const nf = ctx._memoizeIntlObject(Intl.NumberFormat, this.opts);
|
|
return nf.format(this.value);
|
|
} catch (e) {
|
|
// XXX Report the error.
|
|
return this.value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compare the object with another instance of a FluentType.
|
|
*
|
|
* @param {MessageContext} ctx
|
|
* @param {FluentType} other
|
|
* @returns {bool}
|
|
*/
|
|
match(ctx, other) {
|
|
if (other instanceof FluentNumber) {
|
|
return this.value === other.value;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
class FluentDateTime extends FluentType {
|
|
constructor(value, opts) {
|
|
super(new Date(value), opts);
|
|
}
|
|
|
|
toString(ctx) {
|
|
try {
|
|
const dtf = ctx._memoizeIntlObject(Intl.DateTimeFormat, this.opts);
|
|
return dtf.format(this.value);
|
|
} catch (e) {
|
|
// XXX Report the error.
|
|
return this.value;
|
|
}
|
|
}
|
|
}
|
|
|
|
class FluentSymbol extends FluentType {
|
|
toString() {
|
|
return this.value;
|
|
}
|
|
|
|
/**
|
|
* Compare the object with another instance of a FluentType.
|
|
*
|
|
* @param {MessageContext} ctx
|
|
* @param {FluentType} other
|
|
* @returns {bool}
|
|
*/
|
|
match(ctx, other) {
|
|
if (other instanceof FluentSymbol) {
|
|
return this.value === other.value;
|
|
} else if (typeof other === "string") {
|
|
return this.value === other;
|
|
} else if (other instanceof FluentNumber) {
|
|
const pr = ctx._memoizeIntlObject(Intl.PluralRules, other.opts);
|
|
return this.value === pr.select(other.value);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
// CONCATENATED MODULE: ./node_modules/fluent/src/builtins.js
|
|
/**
|
|
* @overview
|
|
*
|
|
* The FTL resolver ships with a number of functions built-in.
|
|
*
|
|
* Each function take two arguments:
|
|
* - args - an array of positional args
|
|
* - opts - an object of key-value args
|
|
*
|
|
* Arguments to functions are guaranteed to already be instances of
|
|
* `FluentType`. Functions must return `FluentType` objects as well.
|
|
*/
|
|
|
|
|
|
|
|
/* harmony default export */ var builtins = ({
|
|
"NUMBER": ([arg], opts) => new FluentNumber(arg.valueOf(), merge(arg.opts, opts)),
|
|
"DATETIME": ([arg], opts) => new FluentDateTime(arg.valueOf(), merge(arg.opts, opts))
|
|
});
|
|
|
|
function merge(argopts, opts) {
|
|
return Object.assign({}, argopts, values(opts));
|
|
}
|
|
|
|
function values(opts) {
|
|
const unwrapped = {};
|
|
for (const [name, opt] of Object.entries(opts)) {
|
|
unwrapped[name] = opt.valueOf();
|
|
}
|
|
return unwrapped;
|
|
}
|
|
// CONCATENATED MODULE: ./node_modules/fluent/src/resolver.js
|
|
/**
|
|
* @overview
|
|
*
|
|
* The role of the Fluent resolver is to format a translation object to an
|
|
* instance of `FluentType` or an array of instances.
|
|
*
|
|
* Translations can contain references to other messages or external arguments,
|
|
* conditional logic in form of select expressions, traits which describe their
|
|
* grammatical features, and can use Fluent builtins which make use of the
|
|
* `Intl` formatters to format numbers, dates, lists and more into the
|
|
* context's language. See the documentation of the Fluent syntax for more
|
|
* information.
|
|
*
|
|
* In case of errors the resolver will try to salvage as much of the
|
|
* translation as possible. In rare situations where the resolver didn't know
|
|
* how to recover from an error it will return an instance of `FluentNone`.
|
|
*
|
|
* `MessageReference`, `VariantExpression`, `AttributeExpression` and
|
|
* `SelectExpression` resolve to raw Runtime Entries objects and the result of
|
|
* the resolution needs to be passed into `Type` to get their real value.
|
|
* This is useful for composing expressions. Consider:
|
|
*
|
|
* brand-name[nominative]
|
|
*
|
|
* which is a `VariantExpression` with properties `id: MessageReference` and
|
|
* `key: Keyword`. If `MessageReference` was resolved eagerly, it would
|
|
* instantly resolve to the value of the `brand-name` message. Instead, we
|
|
* want to get the message object and look for its `nominative` variant.
|
|
*
|
|
* All other expressions (except for `FunctionReference` which is only used in
|
|
* `CallExpression`) resolve to an instance of `FluentType`. The caller should
|
|
* use the `toString` method to convert the instance to a native value.
|
|
*
|
|
*
|
|
* All functions in this file pass around a special object called `env`.
|
|
* This object stores a set of elements used by all resolve functions:
|
|
*
|
|
* * {MessageContext} ctx
|
|
* context for which the given resolution is happening
|
|
* * {Object} args
|
|
* list of developer provided arguments that can be used
|
|
* * {Array} errors
|
|
* list of errors collected while resolving
|
|
* * {WeakSet} dirty
|
|
* Set of patterns already encountered during this resolution.
|
|
* This is used to prevent cyclic resolutions.
|
|
*/
|
|
|
|
|
|
|
|
|
|
// Prevent expansion of too long placeables.
|
|
const MAX_PLACEABLE_LENGTH = 2500;
|
|
|
|
// Unicode bidi isolation characters.
|
|
const FSI = "\u2068";
|
|
const PDI = "\u2069";
|
|
|
|
/**
|
|
* Helper for choosing the default value from a set of members.
|
|
*
|
|
* Used in SelectExpressions and Type.
|
|
*
|
|
* @param {Object} env
|
|
* Resolver environment object.
|
|
* @param {Object} members
|
|
* Hash map of variants from which the default value is to be selected.
|
|
* @param {Number} def
|
|
* The index of the default variant.
|
|
* @returns {FluentType}
|
|
* @private
|
|
*/
|
|
function DefaultMember(env, members, def) {
|
|
if (members[def]) {
|
|
return members[def];
|
|
}
|
|
|
|
const { errors } = env;
|
|
errors.push(new RangeError("No default"));
|
|
return new FluentNone();
|
|
}
|
|
|
|
/**
|
|
* Resolve a reference to another message.
|
|
*
|
|
* @param {Object} env
|
|
* Resolver environment object.
|
|
* @param {Object} id
|
|
* The identifier of the message to be resolved.
|
|
* @param {String} id.name
|
|
* The name of the identifier.
|
|
* @returns {FluentType}
|
|
* @private
|
|
*/
|
|
function MessageReference(env, { name }) {
|
|
const { ctx, errors } = env;
|
|
const message = name.startsWith("-") ? ctx._terms.get(name) : ctx._messages.get(name);
|
|
|
|
if (!message) {
|
|
const err = name.startsWith("-") ? new ReferenceError(`Unknown term: ${name}`) : new ReferenceError(`Unknown message: ${name}`);
|
|
errors.push(err);
|
|
return new FluentNone(name);
|
|
}
|
|
|
|
return message;
|
|
}
|
|
|
|
/**
|
|
* Resolve a variant expression to the variant object.
|
|
*
|
|
* @param {Object} env
|
|
* Resolver environment object.
|
|
* @param {Object} expr
|
|
* An expression to be resolved.
|
|
* @param {Object} expr.id
|
|
* An Identifier of a message for which the variant is resolved.
|
|
* @param {Object} expr.id.name
|
|
* Name a message for which the variant is resolved.
|
|
* @param {Object} expr.key
|
|
* Variant key to be resolved.
|
|
* @returns {FluentType}
|
|
* @private
|
|
*/
|
|
function VariantExpression(env, { id, key }) {
|
|
const message = MessageReference(env, id);
|
|
if (message instanceof FluentNone) {
|
|
return message;
|
|
}
|
|
|
|
const { ctx, errors } = env;
|
|
const keyword = Type(env, key);
|
|
|
|
function isVariantList(node) {
|
|
return Array.isArray(node) && node[0].type === "sel" && node[0].exp === null;
|
|
}
|
|
|
|
if (isVariantList(message.val)) {
|
|
// Match the specified key against keys of each variant, in order.
|
|
for (const variant of message.val[0].vars) {
|
|
const variantKey = Type(env, variant.key);
|
|
if (keyword.match(ctx, variantKey)) {
|
|
return variant;
|
|
}
|
|
}
|
|
}
|
|
|
|
errors.push(new ReferenceError(`Unknown variant: ${keyword.toString(ctx)}`));
|
|
return Type(env, message);
|
|
}
|
|
|
|
/**
|
|
* Resolve an attribute expression to the attribute object.
|
|
*
|
|
* @param {Object} env
|
|
* Resolver environment object.
|
|
* @param {Object} expr
|
|
* An expression to be resolved.
|
|
* @param {String} expr.id
|
|
* An ID of a message for which the attribute is resolved.
|
|
* @param {String} expr.name
|
|
* Name of the attribute to be resolved.
|
|
* @returns {FluentType}
|
|
* @private
|
|
*/
|
|
function AttributeExpression(env, { id, name }) {
|
|
const message = MessageReference(env, id);
|
|
if (message instanceof FluentNone) {
|
|
return message;
|
|
}
|
|
|
|
if (message.attrs) {
|
|
// Match the specified name against keys of each attribute.
|
|
for (const attrName in message.attrs) {
|
|
if (name === attrName) {
|
|
return message.attrs[name];
|
|
}
|
|
}
|
|
}
|
|
|
|
const { errors } = env;
|
|
errors.push(new ReferenceError(`Unknown attribute: ${name}`));
|
|
return Type(env, message);
|
|
}
|
|
|
|
/**
|
|
* Resolve a select expression to the member object.
|
|
*
|
|
* @param {Object} env
|
|
* Resolver environment object.
|
|
* @param {Object} expr
|
|
* An expression to be resolved.
|
|
* @param {String} expr.exp
|
|
* Selector expression
|
|
* @param {Array} expr.vars
|
|
* List of variants for the select expression.
|
|
* @param {Number} expr.def
|
|
* Index of the default variant.
|
|
* @returns {FluentType}
|
|
* @private
|
|
*/
|
|
function SelectExpression(env, { exp, vars, def }) {
|
|
if (exp === null) {
|
|
return DefaultMember(env, vars, def);
|
|
}
|
|
|
|
const selector = Type(env, exp);
|
|
if (selector instanceof FluentNone) {
|
|
return DefaultMember(env, vars, def);
|
|
}
|
|
|
|
// Match the selector against keys of each variant, in order.
|
|
for (const variant of vars) {
|
|
const key = Type(env, variant.key);
|
|
const keyCanMatch = key instanceof FluentNumber || key instanceof FluentSymbol;
|
|
|
|
if (!keyCanMatch) {
|
|
continue;
|
|
}
|
|
|
|
const { ctx } = env;
|
|
|
|
if (key.match(ctx, selector)) {
|
|
return variant;
|
|
}
|
|
}
|
|
|
|
return DefaultMember(env, vars, def);
|
|
}
|
|
|
|
/**
|
|
* Resolve expression to a Fluent type.
|
|
*
|
|
* JavaScript strings are a special case. Since they natively have the
|
|
* `toString` method they can be used as if they were a Fluent type without
|
|
* paying the cost of creating a instance of one.
|
|
*
|
|
* @param {Object} env
|
|
* Resolver environment object.
|
|
* @param {Object} expr
|
|
* An expression object to be resolved into a Fluent type.
|
|
* @returns {FluentType}
|
|
* @private
|
|
*/
|
|
function Type(env, expr) {
|
|
// A fast-path for strings which are the most common case, and for
|
|
// `FluentNone` which doesn't require any additional logic.
|
|
if (typeof expr === "string" || expr instanceof FluentNone) {
|
|
return expr;
|
|
}
|
|
|
|
// The Runtime AST (Entries) encodes patterns (complex strings with
|
|
// placeables) as Arrays.
|
|
if (Array.isArray(expr)) {
|
|
return Pattern(env, expr);
|
|
}
|
|
|
|
switch (expr.type) {
|
|
case "varname":
|
|
return new FluentSymbol(expr.name);
|
|
case "num":
|
|
return new FluentNumber(expr.val);
|
|
case "ext":
|
|
return ExternalArgument(env, expr);
|
|
case "fun":
|
|
return FunctionReference(env, expr);
|
|
case "call":
|
|
return CallExpression(env, expr);
|
|
case "ref":
|
|
{
|
|
const message = MessageReference(env, expr);
|
|
return Type(env, message);
|
|
}
|
|
case "attr":
|
|
{
|
|
const attr = AttributeExpression(env, expr);
|
|
return Type(env, attr);
|
|
}
|
|
case "var":
|
|
{
|
|
const variant = VariantExpression(env, expr);
|
|
return Type(env, variant);
|
|
}
|
|
case "sel":
|
|
{
|
|
const member = SelectExpression(env, expr);
|
|
return Type(env, member);
|
|
}
|
|
case undefined:
|
|
{
|
|
// If it's a node with a value, resolve the value.
|
|
if (expr.val !== null && expr.val !== undefined) {
|
|
return Type(env, expr.val);
|
|
}
|
|
|
|
const { errors } = env;
|
|
errors.push(new RangeError("No value"));
|
|
return new FluentNone();
|
|
}
|
|
default:
|
|
return new FluentNone();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve a reference to an external argument.
|
|
*
|
|
* @param {Object} env
|
|
* Resolver environment object.
|
|
* @param {Object} expr
|
|
* An expression to be resolved.
|
|
* @param {String} expr.name
|
|
* Name of an argument to be returned.
|
|
* @returns {FluentType}
|
|
* @private
|
|
*/
|
|
function ExternalArgument(env, { name }) {
|
|
const { args, errors } = env;
|
|
|
|
if (!args || !args.hasOwnProperty(name)) {
|
|
errors.push(new ReferenceError(`Unknown external: ${name}`));
|
|
return new FluentNone(name);
|
|
}
|
|
|
|
const arg = args[name];
|
|
|
|
// Return early if the argument already is an instance of FluentType.
|
|
if (arg instanceof FluentType) {
|
|
return arg;
|
|
}
|
|
|
|
// Convert the argument to a Fluent type.
|
|
switch (typeof arg) {
|
|
case "string":
|
|
return arg;
|
|
case "number":
|
|
return new FluentNumber(arg);
|
|
case "object":
|
|
if (arg instanceof Date) {
|
|
return new FluentDateTime(arg);
|
|
}
|
|
default:
|
|
errors.push(new TypeError(`Unsupported external type: ${name}, ${typeof arg}`));
|
|
return new FluentNone(name);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve a reference to a function.
|
|
*
|
|
* @param {Object} env
|
|
* Resolver environment object.
|
|
* @param {Object} expr
|
|
* An expression to be resolved.
|
|
* @param {String} expr.name
|
|
* Name of the function to be returned.
|
|
* @returns {Function}
|
|
* @private
|
|
*/
|
|
function FunctionReference(env, { name }) {
|
|
// Some functions are built-in. Others may be provided by the runtime via
|
|
// the `MessageContext` constructor.
|
|
const { ctx: { _functions }, errors } = env;
|
|
const func = _functions[name] || builtins[name];
|
|
|
|
if (!func) {
|
|
errors.push(new ReferenceError(`Unknown function: ${name}()`));
|
|
return new FluentNone(`${name}()`);
|
|
}
|
|
|
|
if (typeof func !== "function") {
|
|
errors.push(new TypeError(`Function ${name}() is not callable`));
|
|
return new FluentNone(`${name}()`);
|
|
}
|
|
|
|
return func;
|
|
}
|
|
|
|
/**
|
|
* Resolve a call to a Function with positional and key-value arguments.
|
|
*
|
|
* @param {Object} env
|
|
* Resolver environment object.
|
|
* @param {Object} expr
|
|
* An expression to be resolved.
|
|
* @param {Object} expr.fun
|
|
* FTL Function object.
|
|
* @param {Array} expr.args
|
|
* FTL Function argument list.
|
|
* @returns {FluentType}
|
|
* @private
|
|
*/
|
|
function CallExpression(env, { fun, args }) {
|
|
const callee = FunctionReference(env, fun);
|
|
|
|
if (callee instanceof FluentNone) {
|
|
return callee;
|
|
}
|
|
|
|
const posargs = [];
|
|
const keyargs = {};
|
|
|
|
for (const arg of args) {
|
|
if (arg.type === "narg") {
|
|
keyargs[arg.name] = Type(env, arg.val);
|
|
} else {
|
|
posargs.push(Type(env, arg));
|
|
}
|
|
}
|
|
|
|
try {
|
|
return callee(posargs, keyargs);
|
|
} catch (e) {
|
|
// XXX Report errors.
|
|
return new FluentNone();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve a pattern (a complex string with placeables).
|
|
*
|
|
* @param {Object} env
|
|
* Resolver environment object.
|
|
* @param {Array} ptn
|
|
* Array of pattern elements.
|
|
* @returns {Array}
|
|
* @private
|
|
*/
|
|
function Pattern(env, ptn) {
|
|
const { ctx, dirty, errors } = env;
|
|
|
|
if (dirty.has(ptn)) {
|
|
errors.push(new RangeError("Cyclic reference"));
|
|
return new FluentNone();
|
|
}
|
|
|
|
// Tag the pattern as dirty for the purpose of the current resolution.
|
|
dirty.add(ptn);
|
|
const result = [];
|
|
|
|
// Wrap interpolations with Directional Isolate Formatting characters
|
|
// only when the pattern has more than one element.
|
|
const useIsolating = ctx._useIsolating && ptn.length > 1;
|
|
|
|
for (const elem of ptn) {
|
|
if (typeof elem === "string") {
|
|
result.push(elem);
|
|
continue;
|
|
}
|
|
|
|
const part = Type(env, elem).toString(ctx);
|
|
|
|
if (useIsolating) {
|
|
result.push(FSI);
|
|
}
|
|
|
|
if (part.length > MAX_PLACEABLE_LENGTH) {
|
|
errors.push(new RangeError("Too many characters in placeable " + `(${part.length}, max allowed is ${MAX_PLACEABLE_LENGTH})`));
|
|
result.push(part.slice(MAX_PLACEABLE_LENGTH));
|
|
} else {
|
|
result.push(part);
|
|
}
|
|
|
|
if (useIsolating) {
|
|
result.push(PDI);
|
|
}
|
|
}
|
|
|
|
dirty.delete(ptn);
|
|
return result.join("");
|
|
}
|
|
|
|
/**
|
|
* Format a translation into a string.
|
|
*
|
|
* @param {MessageContext} ctx
|
|
* A MessageContext instance which will be used to resolve the
|
|
* contextual information of the message.
|
|
* @param {Object} args
|
|
* List of arguments provided by the developer which can be accessed
|
|
* from the message.
|
|
* @param {Object} message
|
|
* An object with the Message to be resolved.
|
|
* @param {Array} errors
|
|
* An error array that any encountered errors will be appended to.
|
|
* @returns {FluentType}
|
|
*/
|
|
function resolve(ctx, args, message, errors = []) {
|
|
const env = {
|
|
ctx, args, errors, dirty: new WeakSet()
|
|
};
|
|
return Type(env, message).toString(ctx);
|
|
}
|
|
// CONCATENATED MODULE: ./node_modules/fluent/src/context.js
|
|
|
|
|
|
|
|
/**
|
|
* Message contexts are single-language stores of translations. They are
|
|
* responsible for parsing translation resources in the Fluent syntax and can
|
|
* format translation units (entities) to strings.
|
|
*
|
|
* Always use `MessageContext.format` to retrieve translation units from
|
|
* a context. Translations can contain references to other entities or
|
|
* external arguments, conditional logic in form of select expressions, traits
|
|
* which describe their grammatical features, and can use Fluent builtins which
|
|
* make use of the `Intl` formatters to format numbers, dates, lists and more
|
|
* into the context's language. See the documentation of the Fluent syntax for
|
|
* more information.
|
|
*/
|
|
class context_MessageContext {
|
|
|
|
/**
|
|
* Create an instance of `MessageContext`.
|
|
*
|
|
* The `locales` argument is used to instantiate `Intl` formatters used by
|
|
* translations. The `options` object can be used to configure the context.
|
|
*
|
|
* Examples:
|
|
*
|
|
* const ctx = new MessageContext(locales);
|
|
*
|
|
* const ctx = new MessageContext(locales, { useIsolating: false });
|
|
*
|
|
* const ctx = new MessageContext(locales, {
|
|
* useIsolating: true,
|
|
* functions: {
|
|
* NODE_ENV: () => process.env.NODE_ENV
|
|
* }
|
|
* });
|
|
*
|
|
* Available options:
|
|
*
|
|
* - `functions` - an object of additional functions available to
|
|
* translations as builtins.
|
|
*
|
|
* - `useIsolating` - boolean specifying whether to use Unicode isolation
|
|
* marks (FSI, PDI) for bidi interpolations.
|
|
*
|
|
* @param {string|Array<string>} locales - Locale or locales of the context
|
|
* @param {Object} [options]
|
|
* @returns {MessageContext}
|
|
*/
|
|
constructor(locales, { functions = {}, useIsolating = true } = {}) {
|
|
this.locales = Array.isArray(locales) ? locales : [locales];
|
|
|
|
this._terms = new Map();
|
|
this._messages = new Map();
|
|
this._functions = functions;
|
|
this._useIsolating = useIsolating;
|
|
this._intls = new WeakMap();
|
|
}
|
|
|
|
/*
|
|
* Return an iterator over public `[id, message]` pairs.
|
|
*
|
|
* @returns {Iterator}
|
|
*/
|
|
get messages() {
|
|
return this._messages[Symbol.iterator]();
|
|
}
|
|
|
|
/*
|
|
* Check if a message is present in the context.
|
|
*
|
|
* @param {string} id - The identifier of the message to check.
|
|
* @returns {bool}
|
|
*/
|
|
hasMessage(id) {
|
|
return this._messages.has(id);
|
|
}
|
|
|
|
/*
|
|
* Return the internal representation of a message.
|
|
*
|
|
* The internal representation should only be used as an argument to
|
|
* `MessageContext.format`.
|
|
*
|
|
* @param {string} id - The identifier of the message to check.
|
|
* @returns {Any}
|
|
*/
|
|
getMessage(id) {
|
|
return this._messages.get(id);
|
|
}
|
|
|
|
/**
|
|
* Add a translation resource to the context.
|
|
*
|
|
* The translation resource must use the Fluent syntax. It will be parsed by
|
|
* the context and each translation unit (message) will be available in the
|
|
* context by its identifier.
|
|
*
|
|
* ctx.addMessages('foo = Foo');
|
|
* ctx.getMessage('foo');
|
|
*
|
|
* // Returns a raw representation of the 'foo' message.
|
|
*
|
|
* Parsed entities should be formatted with the `format` method in case they
|
|
* contain logic (references, select expressions etc.).
|
|
*
|
|
* @param {string} source - Text resource with translations.
|
|
* @returns {Array<Error>}
|
|
*/
|
|
addMessages(source) {
|
|
const [entries, errors] = parse(source);
|
|
for (const id in entries) {
|
|
if (id.startsWith("-")) {
|
|
// Identifiers starting with a dash (-) define terms. Terms are private
|
|
// and cannot be retrieved from MessageContext.
|
|
if (this._terms.has(id)) {
|
|
errors.push(`Attempt to override an existing term: "${id}"`);
|
|
continue;
|
|
}
|
|
this._terms.set(id, entries[id]);
|
|
} else {
|
|
if (this._messages.has(id)) {
|
|
errors.push(`Attempt to override an existing message: "${id}"`);
|
|
continue;
|
|
}
|
|
this._messages.set(id, entries[id]);
|
|
}
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
/**
|
|
* Format a message to a string or null.
|
|
*
|
|
* Format a raw `message` from the context into a string (or a null if it has
|
|
* a null value). `args` will be used to resolve references to external
|
|
* arguments inside of the translation.
|
|
*
|
|
* In case of errors `format` will try to salvage as much of the translation
|
|
* as possible and will still return a string. For performance reasons, the
|
|
* encountered errors are not returned but instead are appended to the
|
|
* `errors` array passed as the third argument.
|
|
*
|
|
* const errors = [];
|
|
* ctx.addMessages('hello = Hello, { $name }!');
|
|
* const hello = ctx.getMessage('hello');
|
|
* ctx.format(hello, { name: 'Jane' }, errors);
|
|
*
|
|
* // Returns 'Hello, Jane!' and `errors` is empty.
|
|
*
|
|
* ctx.format(hello, undefined, errors);
|
|
*
|
|
* // Returns 'Hello, name!' and `errors` is now:
|
|
*
|
|
* [<ReferenceError: Unknown external: name>]
|
|
*
|
|
* @param {Object | string} message
|
|
* @param {Object | undefined} args
|
|
* @param {Array} errors
|
|
* @returns {?string}
|
|
*/
|
|
format(message, args, errors) {
|
|
// optimize entities which are simple strings with no attributes
|
|
if (typeof message === "string") {
|
|
return message;
|
|
}
|
|
|
|
// optimize simple-string entities with attributes
|
|
if (typeof message.val === "string") {
|
|
return message.val;
|
|
}
|
|
|
|
// optimize entities with null values
|
|
if (message.val === undefined) {
|
|
return null;
|
|
}
|
|
|
|
return resolve(this, args, message, errors);
|
|
}
|
|
|
|
_memoizeIntlObject(ctor, opts) {
|
|
const cache = this._intls.get(ctor) || {};
|
|
const id = JSON.stringify(opts);
|
|
|
|
if (!cache[id]) {
|
|
cache[id] = new ctor(this.locales, opts);
|
|
this._intls.set(ctor, cache);
|
|
}
|
|
|
|
return cache[id];
|
|
}
|
|
}
|
|
// CONCATENATED MODULE: ./node_modules/fluent/src/cached_iterable.js
|
|
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
|
|
|
|
/*
|
|
* CachedIterable caches the elements yielded by an iterable.
|
|
*
|
|
* It can be used to iterate over an iterable many times without depleting the
|
|
* iterable.
|
|
*/
|
|
class CachedIterable {
|
|
/**
|
|
* Create an `CachedIterable` instance.
|
|
*
|
|
* @param {Iterable} iterable
|
|
* @returns {CachedIterable}
|
|
*/
|
|
constructor(iterable) {
|
|
if (Symbol.asyncIterator in Object(iterable)) {
|
|
this.iterator = iterable[Symbol.asyncIterator]();
|
|
} else if (Symbol.iterator in Object(iterable)) {
|
|
this.iterator = iterable[Symbol.iterator]();
|
|
} else {
|
|
throw new TypeError("Argument must implement the iteration protocol.");
|
|
}
|
|
|
|
this.seen = [];
|
|
}
|
|
|
|
[Symbol.iterator]() {
|
|
const { seen, iterator } = this;
|
|
let cur = 0;
|
|
|
|
return {
|
|
next() {
|
|
if (seen.length <= cur) {
|
|
seen.push(iterator.next());
|
|
}
|
|
return seen[cur++];
|
|
}
|
|
};
|
|
}
|
|
|
|
[Symbol.asyncIterator]() {
|
|
const { seen, iterator } = this;
|
|
let cur = 0;
|
|
|
|
return {
|
|
next() {
|
|
return _asyncToGenerator(function* () {
|
|
if (seen.length <= cur) {
|
|
seen.push((yield iterator.next()));
|
|
}
|
|
return seen[cur++];
|
|
})();
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* This method allows user to consume the next element from the iterator
|
|
* into the cache.
|
|
*/
|
|
touchNext() {
|
|
const { seen, iterator } = this;
|
|
if (seen.length === 0 || seen[seen.length - 1].done === false) {
|
|
seen.push(iterator.next());
|
|
}
|
|
}
|
|
}
|
|
// CONCATENATED MODULE: ./node_modules/fluent/src/fallback.js
|
|
function _asyncIterator(iterable) { if (typeof Symbol === "function") { if (Symbol.asyncIterator) { var method = iterable[Symbol.asyncIterator]; if (method != null) return method.call(iterable); } if (Symbol.iterator) { return iterable[Symbol.iterator](); } } throw new TypeError("Object is not async iterable"); }
|
|
|
|
function fallback_asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
|
|
|
|
/*
|
|
* @overview
|
|
*
|
|
* Functions for managing ordered sequences of MessageContexts.
|
|
*
|
|
* An ordered iterable of MessageContext instances can represent the current
|
|
* negotiated fallback chain of languages. This iterable can be used to find
|
|
* the best existing translation for a given identifier.
|
|
*
|
|
* The mapContext* methods can be used to find the first MessageContext in the
|
|
* given iterable which contains the translation with the given identifier. If
|
|
* the iterable is ordered according to the result of a language negotiation
|
|
* the returned MessageContext contains the best available translation.
|
|
*
|
|
* A simple function which formats translations based on the identifier might
|
|
* be implemented as follows:
|
|
*
|
|
* formatString(id, args) {
|
|
* const ctx = mapContextSync(contexts, id);
|
|
*
|
|
* if (ctx === null) {
|
|
* return id;
|
|
* }
|
|
*
|
|
* const msg = ctx.getMessage(id);
|
|
* return ctx.format(msg, args);
|
|
* }
|
|
*
|
|
* In order to pass an iterator to mapContext*, wrap it in CachedIterable.
|
|
* This allows multiple calls to mapContext* without advancing and eventually
|
|
* depleting the iterator.
|
|
*
|
|
* function *generateMessages() {
|
|
* // Some lazy logic for yielding MessageContexts.
|
|
* yield *[ctx1, ctx2];
|
|
* }
|
|
*
|
|
* const contexts = new CachedIterable(generateMessages());
|
|
* const ctx = mapContextSync(contexts, id);
|
|
*
|
|
*/
|
|
|
|
/*
|
|
* Synchronously map an identifier or an array of identifiers to the best
|
|
* `MessageContext` instance(s).
|
|
*
|
|
* @param {Iterable} iterable
|
|
* @param {string|Array<string>} ids
|
|
* @returns {MessageContext|Array<MessageContext>}
|
|
*/
|
|
function mapContextSync(iterable, ids) {
|
|
if (!Array.isArray(ids)) {
|
|
return getContextForId(iterable, ids);
|
|
}
|
|
|
|
return ids.map(id => getContextForId(iterable, id));
|
|
}
|
|
|
|
/*
|
|
* Find the best `MessageContext` with the translation for `id`.
|
|
*/
|
|
function getContextForId(iterable, id) {
|
|
for (const context of iterable) {
|
|
if (context.hasMessage(id)) {
|
|
return context;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/*
|
|
* Asynchronously map an identifier or an array of identifiers to the best
|
|
* `MessageContext` instance(s).
|
|
*
|
|
* @param {AsyncIterable} iterable
|
|
* @param {string|Array<string>} ids
|
|
* @returns {Promise<MessageContext|Array<MessageContext>>}
|
|
*/
|
|
let mapContextAsync = (() => {
|
|
var _ref = fallback_asyncToGenerator(function* (iterable, ids) {
|
|
if (!Array.isArray(ids)) {
|
|
var _iteratorNormalCompletion = true;
|
|
var _didIteratorError = false;
|
|
var _iteratorError = undefined;
|
|
|
|
try {
|
|
for (var _iterator = _asyncIterator(iterable), _step, _value; _step = yield _iterator.next(), _iteratorNormalCompletion = _step.done, _value = yield _step.value, !_iteratorNormalCompletion; _iteratorNormalCompletion = true) {
|
|
const context = _value;
|
|
|
|
if (context.hasMessage(ids)) {
|
|
return context;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
_didIteratorError = true;
|
|
_iteratorError = err;
|
|
} finally {
|
|
try {
|
|
if (!_iteratorNormalCompletion && _iterator.return) {
|
|
yield _iterator.return();
|
|
}
|
|
} finally {
|
|
if (_didIteratorError) {
|
|
throw _iteratorError;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let remainingCount = ids.length;
|
|
const foundContexts = new Array(remainingCount).fill(null);
|
|
|
|
var _iteratorNormalCompletion2 = true;
|
|
var _didIteratorError2 = false;
|
|
var _iteratorError2 = undefined;
|
|
|
|
try {
|
|
for (var _iterator2 = _asyncIterator(iterable), _step2, _value2; _step2 = yield _iterator2.next(), _iteratorNormalCompletion2 = _step2.done, _value2 = yield _step2.value, !_iteratorNormalCompletion2; _iteratorNormalCompletion2 = true) {
|
|
const context = _value2;
|
|
|
|
// XXX Switch to const [index, id] of id.entries() when we move to Babel 7.
|
|
// See https://github.com/babel/babel/issues/5880.
|
|
for (let index = 0; index < ids.length; index++) {
|
|
const id = ids[index];
|
|
if (!foundContexts[index] && context.hasMessage(id)) {
|
|
foundContexts[index] = context;
|
|
remainingCount--;
|
|
}
|
|
|
|
// Return early when all ids have been mapped to contexts.
|
|
if (remainingCount === 0) {
|
|
return foundContexts;
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
_didIteratorError2 = true;
|
|
_iteratorError2 = err;
|
|
} finally {
|
|
try {
|
|
if (!_iteratorNormalCompletion2 && _iterator2.return) {
|
|
yield _iterator2.return();
|
|
}
|
|
} finally {
|
|
if (_didIteratorError2) {
|
|
throw _iteratorError2;
|
|
}
|
|
}
|
|
}
|
|
|
|
return foundContexts;
|
|
});
|
|
|
|
return function mapContextAsync(_x, _x2) {
|
|
return _ref.apply(this, arguments);
|
|
};
|
|
})();
|
|
// CONCATENATED MODULE: ./node_modules/fluent/src/util.js
|
|
function nonBlank(line) {
|
|
return !/^\s*$/.test(line);
|
|
}
|
|
|
|
function countIndent(line) {
|
|
const [indent] = line.match(/^\s*/);
|
|
return indent.length;
|
|
}
|
|
|
|
/**
|
|
* Template literal tag for dedenting FTL code.
|
|
*
|
|
* Strip the common indent of non-blank lines. Remove blank lines.
|
|
*
|
|
* @param {Array<string>} strings
|
|
*/
|
|
function ftl(strings) {
|
|
const [code] = strings;
|
|
const lines = code.split("\n").filter(nonBlank);
|
|
const indents = lines.map(countIndent);
|
|
const common = Math.min(...indents);
|
|
const indent = new RegExp(`^\\s{${common}}`);
|
|
|
|
return lines.map(line => line.replace(indent, "")).join("\n");
|
|
}
|
|
// CONCATENATED MODULE: ./node_modules/fluent/src/index.js
|
|
/* concated harmony reexport */__webpack_require__.d(__webpack_exports__, "_parse", function() { return parse; });
|
|
/* concated harmony reexport */__webpack_require__.d(__webpack_exports__, "MessageContext", function() { return context_MessageContext; });
|
|
/* concated harmony reexport */__webpack_require__.d(__webpack_exports__, "MessageArgument", function() { return FluentType; });
|
|
/* concated harmony reexport */__webpack_require__.d(__webpack_exports__, "MessageNumberArgument", function() { return FluentNumber; });
|
|
/* concated harmony reexport */__webpack_require__.d(__webpack_exports__, "MessageDateTimeArgument", function() { return FluentDateTime; });
|
|
/* concated harmony reexport */__webpack_require__.d(__webpack_exports__, "CachedIterable", function() { return CachedIterable; });
|
|
/* concated harmony reexport */__webpack_require__.d(__webpack_exports__, "mapContextSync", function() { return mapContextSync; });
|
|
/* concated harmony reexport */__webpack_require__.d(__webpack_exports__, "mapContextAsync", function() { return mapContextAsync; });
|
|
/* concated harmony reexport */__webpack_require__.d(__webpack_exports__, "ftl", function() { return ftl; });
|
|
/*
|
|
* @module fluent
|
|
* @overview
|
|
*
|
|
* `fluent` is a JavaScript implementation of Project Fluent, a localization
|
|
* framework designed to unleash the expressive power of the natural language.
|
|
*
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/***/ }),
|
|
/* 44 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
|
|
// EXTERNAL MODULE: external "React"
|
|
var external_React_ = __webpack_require__(9);
|
|
|
|
// EXTERNAL MODULE: external "PropTypes"
|
|
var external_PropTypes_ = __webpack_require__(10);
|
|
var external_PropTypes_default = /*#__PURE__*/__webpack_require__.n(external_PropTypes_);
|
|
|
|
// EXTERNAL MODULE: ./node_modules/fluent/src/index.js + 8 modules
|
|
var src = __webpack_require__(43);
|
|
|
|
// CONCATENATED MODULE: ./node_modules/fluent-react/src/localization.js
|
|
|
|
|
|
/*
|
|
* `ReactLocalization` handles translation formatting and fallback.
|
|
*
|
|
* The current negotiated fallback chain of languages is stored in the
|
|
* `ReactLocalization` instance in form of an iterable of `MessageContext`
|
|
* instances. This iterable is used to find the best existing translation for
|
|
* a given identifier.
|
|
*
|
|
* `Localized` components must subscribe to the changes of the
|
|
* `ReactLocalization`'s fallback chain. When the fallback chain changes (the
|
|
* `messages` iterable is set anew), all subscribed compontent must relocalize.
|
|
*
|
|
* The `ReactLocalization` class instances are exposed to `Localized` elements
|
|
* via the `LocalizationProvider` component.
|
|
*/
|
|
class localization_ReactLocalization {
|
|
constructor(messages) {
|
|
this.contexts = new src["CachedIterable"](messages);
|
|
this.subs = new Set();
|
|
}
|
|
|
|
/*
|
|
* Subscribe a `Localized` component to changes of `messages`.
|
|
*/
|
|
subscribe(comp) {
|
|
this.subs.add(comp);
|
|
}
|
|
|
|
/*
|
|
* Unsubscribe a `Localized` component from `messages` changes.
|
|
*/
|
|
unsubscribe(comp) {
|
|
this.subs.delete(comp);
|
|
}
|
|
|
|
/*
|
|
* Set a new `messages` iterable and trigger the retranslation.
|
|
*/
|
|
setMessages(messages) {
|
|
this.contexts = new src["CachedIterable"](messages);
|
|
|
|
// Update all subscribed Localized components.
|
|
this.subs.forEach(comp => comp.relocalize());
|
|
}
|
|
|
|
getMessageContext(id) {
|
|
return Object(src["mapContextSync"])(this.contexts, id);
|
|
}
|
|
|
|
formatCompound(mcx, msg, args) {
|
|
const value = mcx.format(msg, args);
|
|
|
|
if (msg.attrs) {
|
|
var attrs = {};
|
|
for (const name of Object.keys(msg.attrs)) {
|
|
attrs[name] = mcx.format(msg.attrs[name], args);
|
|
}
|
|
}
|
|
|
|
return { value, attrs };
|
|
}
|
|
|
|
/*
|
|
* Find a translation by `id` and format it to a string using `args`.
|
|
*/
|
|
getString(id, args, fallback) {
|
|
const mcx = this.getMessageContext(id);
|
|
|
|
if (mcx === null) {
|
|
return fallback || id;
|
|
}
|
|
|
|
const msg = mcx.getMessage(id);
|
|
return mcx.format(msg, args);
|
|
}
|
|
}
|
|
|
|
function isReactLocalization(props, propName) {
|
|
const prop = props[propName];
|
|
|
|
if (prop instanceof localization_ReactLocalization) {
|
|
return null;
|
|
}
|
|
|
|
return new Error(`The ${propName} context field must be an instance of ReactLocalization.`);
|
|
}
|
|
// CONCATENATED MODULE: ./node_modules/fluent-react/src/provider.js
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
* The Provider component for the `ReactLocalization` class.
|
|
*
|
|
* Exposes a `ReactLocalization` instance to all descendants via React's
|
|
* context feature. It makes translations available to all localizable
|
|
* elements in the descendant's render tree without the need to pass them
|
|
* explicitly.
|
|
*
|
|
* <LocalizationProvider messages={…}>
|
|
* …
|
|
* </LocalizationProvider>
|
|
*
|
|
* The `LocalizationProvider` component takes one prop: `messages`. It should
|
|
* be an iterable of `MessageContext` instances in order of the user's
|
|
* preferred languages. The `MessageContext` instances will be used by
|
|
* `ReactLocalization` to format translations. If a translation is missing in
|
|
* one instance, `ReactLocalization` will fall back to the next one.
|
|
*/
|
|
class provider_LocalizationProvider extends external_React_["Component"] {
|
|
constructor(props) {
|
|
super(props);
|
|
const { messages } = props;
|
|
|
|
if (messages === undefined) {
|
|
throw new Error("LocalizationProvider must receive the messages prop.");
|
|
}
|
|
|
|
if (!messages[Symbol.iterator]) {
|
|
throw new Error("The messages prop must be an iterable.");
|
|
}
|
|
|
|
this.l10n = new localization_ReactLocalization(messages);
|
|
}
|
|
|
|
getChildContext() {
|
|
return {
|
|
l10n: this.l10n
|
|
};
|
|
}
|
|
|
|
componentWillReceiveProps(next) {
|
|
const { messages } = next;
|
|
|
|
if (messages !== this.props.messages) {
|
|
this.l10n.setMessages(messages);
|
|
}
|
|
}
|
|
|
|
render() {
|
|
return external_React_["Children"].only(this.props.children);
|
|
}
|
|
}
|
|
|
|
provider_LocalizationProvider.childContextTypes = {
|
|
l10n: isReactLocalization
|
|
};
|
|
|
|
provider_LocalizationProvider.propTypes = {
|
|
children: external_PropTypes_default.a.element.isRequired,
|
|
messages: isIterable
|
|
};
|
|
|
|
function isIterable(props, propName, componentName) {
|
|
const prop = props[propName];
|
|
|
|
if (Symbol.iterator in Object(prop)) {
|
|
return null;
|
|
}
|
|
|
|
return new Error(`The ${propName} prop supplied to ${componentName} must be an iterable.`);
|
|
}
|
|
// CONCATENATED MODULE: ./node_modules/fluent-react/src/with_localization.js
|
|
|
|
|
|
|
|
|
|
function withLocalization(Inner) {
|
|
class WithLocalization extends external_React_["Component"] {
|
|
componentDidMount() {
|
|
const { l10n } = this.context;
|
|
|
|
if (l10n) {
|
|
l10n.subscribe(this);
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
const { l10n } = this.context;
|
|
|
|
if (l10n) {
|
|
l10n.unsubscribe(this);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Rerender this component in a new language.
|
|
*/
|
|
relocalize() {
|
|
// When the `ReactLocalization`'s fallback chain changes, update the
|
|
// component.
|
|
this.forceUpdate();
|
|
}
|
|
|
|
/*
|
|
* Find a translation by `id` and format it to a string using `args`.
|
|
*/
|
|
getString(id, args, fallback) {
|
|
const { l10n } = this.context;
|
|
|
|
if (!l10n) {
|
|
return fallback || id;
|
|
}
|
|
|
|
return l10n.getString(id, args, fallback);
|
|
}
|
|
|
|
render() {
|
|
return Object(external_React_["createElement"])(Inner, Object.assign(
|
|
// getString needs to be re-bound on updates to trigger a re-render
|
|
{ getString: (...args) => this.getString(...args) }, this.props));
|
|
}
|
|
}
|
|
|
|
WithLocalization.displayName = `WithLocalization(${displayName(Inner)})`;
|
|
|
|
WithLocalization.contextTypes = {
|
|
l10n: isReactLocalization
|
|
};
|
|
|
|
return WithLocalization;
|
|
}
|
|
|
|
function displayName(component) {
|
|
return component.displayName || component.name || "Component";
|
|
}
|
|
// CONCATENATED MODULE: ./node_modules/fluent-react/src/markup.js
|
|
/* eslint-env browser */
|
|
|
|
const TEMPLATE = document.createElement("template");
|
|
|
|
function parseMarkup(str) {
|
|
TEMPLATE.innerHTML = str;
|
|
return TEMPLATE.content;
|
|
}
|
|
// CONCATENATED MODULE: ./node_modules/fluent-react/vendor/omittedCloseTags.js
|
|
/**
|
|
* Copyright (c) 2013-present, Facebook, Inc.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in this directory.
|
|
*/
|
|
|
|
// For HTML, certain tags should omit their close tag. We keep a whitelist for
|
|
// those special-case tags.
|
|
|
|
var omittedCloseTags = {
|
|
area: true,
|
|
base: true,
|
|
br: true,
|
|
col: true,
|
|
embed: true,
|
|
hr: true,
|
|
img: true,
|
|
input: true,
|
|
keygen: true,
|
|
link: true,
|
|
meta: true,
|
|
param: true,
|
|
source: true,
|
|
track: true,
|
|
wbr: true
|
|
// NOTE: menuitem's close tag should be omitted, but that causes problems.
|
|
};
|
|
|
|
/* harmony default export */ var vendor_omittedCloseTags = (omittedCloseTags);
|
|
// CONCATENATED MODULE: ./node_modules/fluent-react/vendor/voidElementTags.js
|
|
/**
|
|
* Copyright (c) 2013-present, Facebook, Inc.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in this directory.
|
|
*/
|
|
|
|
|
|
|
|
// For HTML, certain tags cannot have children. This has the same purpose as
|
|
// `omittedCloseTags` except that `menuitem` should still have its closing tag.
|
|
|
|
var voidElementTags = Object.assign({
|
|
menuitem: true
|
|
}, vendor_omittedCloseTags);
|
|
|
|
/* harmony default export */ var vendor_voidElementTags = (voidElementTags);
|
|
// CONCATENATED MODULE: ./node_modules/fluent-react/src/localized.js
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Match the opening angle bracket (<) in HTML tags, and HTML entities like
|
|
// &, &, &.
|
|
const reMarkup = /<|&#?\w+;/;
|
|
|
|
/*
|
|
* Prepare props passed to `Localized` for formatting.
|
|
*/
|
|
function toArguments(props) {
|
|
const args = {};
|
|
const elems = {};
|
|
|
|
for (const [propname, propval] of Object.entries(props)) {
|
|
if (propname.startsWith("$")) {
|
|
const name = propname.substr(1);
|
|
args[name] = propval;
|
|
} else if (Object(external_React_["isValidElement"])(propval)) {
|
|
// We'll try to match localNames of elements found in the translation with
|
|
// names of elements passed as props. localNames are always lowercase.
|
|
const name = propname.toLowerCase();
|
|
elems[name] = propval;
|
|
}
|
|
}
|
|
|
|
return [args, elems];
|
|
}
|
|
|
|
/*
|
|
* The `Localized` class renders its child with translated props and children.
|
|
*
|
|
* <Localized id="hello-world">
|
|
* <p>{'Hello, world!'}</p>
|
|
* </Localized>
|
|
*
|
|
* The `id` prop should be the unique identifier of the translation. Any
|
|
* attributes found in the translation will be applied to the wrapped element.
|
|
*
|
|
* Arguments to the translation can be passed as `$`-prefixed props on
|
|
* `Localized`.
|
|
*
|
|
* <Localized id="hello-world" $username={name}>
|
|
* <p>{'Hello, { $username }!'}</p>
|
|
* </Localized>
|
|
*
|
|
* It's recommended that the contents of the wrapped component be a string
|
|
* expression. The string will be used as the ultimate fallback if no
|
|
* translation is available. It also makes it easy to grep for strings in the
|
|
* source code.
|
|
*/
|
|
class localized_Localized extends external_React_["Component"] {
|
|
componentDidMount() {
|
|
const { l10n } = this.context;
|
|
|
|
if (l10n) {
|
|
l10n.subscribe(this);
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
const { l10n } = this.context;
|
|
|
|
if (l10n) {
|
|
l10n.unsubscribe(this);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Rerender this component in a new language.
|
|
*/
|
|
relocalize() {
|
|
// When the `ReactLocalization`'s fallback chain changes, update the
|
|
// component.
|
|
this.forceUpdate();
|
|
}
|
|
|
|
render() {
|
|
const { l10n } = this.context;
|
|
const { id, attrs, children } = this.props;
|
|
const elem = external_React_["Children"].only(children);
|
|
|
|
if (!l10n) {
|
|
// Use the wrapped component as fallback.
|
|
return elem;
|
|
}
|
|
|
|
const mcx = l10n.getMessageContext(id);
|
|
|
|
if (mcx === null) {
|
|
// Use the wrapped component as fallback.
|
|
return elem;
|
|
}
|
|
|
|
const msg = mcx.getMessage(id);
|
|
const [args, elems] = toArguments(this.props);
|
|
const {
|
|
value: messageValue,
|
|
attrs: messageAttrs
|
|
} = l10n.formatCompound(mcx, msg, args);
|
|
|
|
// The default is to forbid all message attributes. If the attrs prop exists
|
|
// on the Localized instance, only set message attributes which have been
|
|
// explicitly allowed by the developer.
|
|
if (attrs && messageAttrs) {
|
|
var localizedProps = {};
|
|
|
|
for (const [name, value] of Object.entries(messageAttrs)) {
|
|
if (attrs[name]) {
|
|
localizedProps[name] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the wrapped component is a known void element, explicitly dismiss the
|
|
// message value and do not pass it to cloneElement in order to avoid the
|
|
// "void element tags must neither have `children` nor use
|
|
// `dangerouslySetInnerHTML`" error.
|
|
if (elem.type in vendor_voidElementTags) {
|
|
return Object(external_React_["cloneElement"])(elem, localizedProps);
|
|
}
|
|
|
|
// If the message has a null value, we're only interested in its attributes.
|
|
// Do not pass the null value to cloneElement as it would nuke all children
|
|
// of the wrapped component.
|
|
if (messageValue === null) {
|
|
return Object(external_React_["cloneElement"])(elem, localizedProps);
|
|
}
|
|
|
|
// If the message value doesn't contain any markup nor any HTML entities,
|
|
// insert it as the only child of the wrapped component.
|
|
if (!reMarkup.test(messageValue)) {
|
|
return Object(external_React_["cloneElement"])(elem, localizedProps, messageValue);
|
|
}
|
|
|
|
// If the message contains markup, parse it and try to match the children
|
|
// found in the translation with the props passed to this Localized.
|
|
const translationNodes = Array.from(parseMarkup(messageValue).childNodes);
|
|
const translatedChildren = translationNodes.map(childNode => {
|
|
if (childNode.nodeType === childNode.TEXT_NODE) {
|
|
return childNode.textContent;
|
|
}
|
|
|
|
// If the child is not expected just take its textContent.
|
|
if (!elems.hasOwnProperty(childNode.localName)) {
|
|
return childNode.textContent;
|
|
}
|
|
|
|
const sourceChild = elems[childNode.localName];
|
|
|
|
// If the element passed as a prop to <Localized> is a known void element,
|
|
// explicitly dismiss any textContent which might have accidentally been
|
|
// defined in the translation to prevent the "void element tags must not
|
|
// have children" error.
|
|
if (sourceChild.type in vendor_voidElementTags) {
|
|
return sourceChild;
|
|
}
|
|
|
|
// TODO Protect contents of elements wrapped in <Localized>
|
|
// https://github.com/projectfluent/fluent.js/issues/184
|
|
// TODO Control localizable attributes on elements passed as props
|
|
// https://github.com/projectfluent/fluent.js/issues/185
|
|
return Object(external_React_["cloneElement"])(sourceChild, null, childNode.textContent);
|
|
});
|
|
|
|
return Object(external_React_["cloneElement"])(elem, localizedProps, ...translatedChildren);
|
|
}
|
|
}
|
|
|
|
localized_Localized.contextTypes = {
|
|
l10n: isReactLocalization
|
|
};
|
|
|
|
localized_Localized.propTypes = {
|
|
children: external_PropTypes_default.a.element.isRequired
|
|
};
|
|
// CONCATENATED MODULE: ./node_modules/fluent-react/src/index.js
|
|
/* concated harmony reexport */__webpack_require__.d(__webpack_exports__, "LocalizationProvider", function() { return provider_LocalizationProvider; });
|
|
/* concated harmony reexport */__webpack_require__.d(__webpack_exports__, "withLocalization", function() { return withLocalization; });
|
|
/* concated harmony reexport */__webpack_require__.d(__webpack_exports__, "Localized", function() { return localized_Localized; });
|
|
/* concated harmony reexport */__webpack_require__.d(__webpack_exports__, "ReactLocalization", function() { return localization_ReactLocalization; });
|
|
/* concated harmony reexport */__webpack_require__.d(__webpack_exports__, "isReactLocalization", function() { return isReactLocalization; });
|
|
/*
|
|
* @module fluent-react
|
|
* @overview
|
|
*
|
|
|
|
* `fluent-react` provides React bindings for Fluent. It takes advantage of
|
|
* React's Components system and the virtual DOM. Translations are exposed to
|
|
* components via the provider pattern.
|
|
*
|
|
* <LocalizationProvider messages={…}>
|
|
* <Localized id="hello-world">
|
|
* <p>{'Hello, world!'}</p>
|
|
* </Localized>
|
|
* </LocalizationProvider>
|
|
*
|
|
* Consult the documentation of the `LocalizationProvider` and the `Localized`
|
|
* components for more information.
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/***/ }),
|
|
/* 45 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
|
|
// EXTERNAL MODULE: ./common/Actions.jsm
|
|
var Actions = __webpack_require__(2);
|
|
|
|
// CONCATENATED MODULE: ./common/Dedupe.jsm
|
|
class Dedupe {
|
|
constructor(createKey) {
|
|
this.createKey = createKey || this.defaultCreateKey;
|
|
}
|
|
|
|
defaultCreateKey(item) {
|
|
return item;
|
|
}
|
|
|
|
/**
|
|
* Dedupe any number of grouped elements favoring those from earlier groups.
|
|
*
|
|
* @param {Array} groups Contains an arbitrary number of arrays of elements.
|
|
* @returns {Array} A matching array of each provided group deduped.
|
|
*/
|
|
group(...groups) {
|
|
const globalKeys = new Set();
|
|
const result = [];
|
|
for (const values of groups) {
|
|
const valueMap = new Map();
|
|
for (const value of values) {
|
|
const key = this.createKey(value);
|
|
if (!globalKeys.has(key) && !valueMap.has(key)) {
|
|
valueMap.set(key, value);
|
|
}
|
|
}
|
|
result.push(valueMap);
|
|
valueMap.forEach((value, key) => globalKeys.add(key));
|
|
}
|
|
return result.map(m => Array.from(m.values()));
|
|
}
|
|
}
|
|
// CONCATENATED MODULE: ./common/Reducers.jsm
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TOP_SITES_DEFAULT_ROWS", function() { return TOP_SITES_DEFAULT_ROWS; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TOP_SITES_MAX_SITES_PER_ROW", function() { return TOP_SITES_MAX_SITES_PER_ROW; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "INITIAL_STATE", function() { return INITIAL_STATE; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "insertPinned", function() { return insertPinned; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "reducers", function() { return reducers; });
|
|
/* 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/. */
|
|
|
|
|
|
|
|
|
|
const TOP_SITES_DEFAULT_ROWS = 1;
|
|
const TOP_SITES_MAX_SITES_PER_ROW = 8;
|
|
|
|
|
|
const dedupe = new Dedupe(site => site && site.url);
|
|
|
|
const INITIAL_STATE = {
|
|
App: {
|
|
// Have we received real data from the app yet?
|
|
initialized: false
|
|
},
|
|
ASRouter: {
|
|
initialized: false,
|
|
allowLegacySnippets: null
|
|
},
|
|
Snippets: { initialized: false },
|
|
TopSites: {
|
|
// Have we received real data from history yet?
|
|
initialized: false,
|
|
// The history (and possibly default) links
|
|
rows: [],
|
|
// Used in content only to dispatch action to TopSiteForm.
|
|
editForm: null,
|
|
// Used in content only to open the SearchShortcutsForm modal.
|
|
showSearchShortcutsForm: false,
|
|
// The list of available search shortcuts.
|
|
searchShortcuts: []
|
|
},
|
|
Prefs: {
|
|
initialized: false,
|
|
values: {}
|
|
},
|
|
Dialog: {
|
|
visible: false,
|
|
data: {}
|
|
},
|
|
Sections: [],
|
|
Pocket: {
|
|
isUserLoggedIn: null,
|
|
pocketCta: {},
|
|
waitingForSpoc: true
|
|
}
|
|
};
|
|
|
|
|
|
function App(prevState = INITIAL_STATE.App, action) {
|
|
switch (action.type) {
|
|
case Actions["actionTypes"].INIT:
|
|
return Object.assign({}, prevState, action.data || {}, { initialized: true });
|
|
default:
|
|
return prevState;
|
|
}
|
|
}
|
|
|
|
function ASRouter(prevState = INITIAL_STATE.ASRouter, action) {
|
|
switch (action.type) {
|
|
case Actions["actionTypes"].AS_ROUTER_INITIALIZED:
|
|
return Object.assign({}, action.data, { initialized: true });
|
|
case Actions["actionTypes"].AS_ROUTER_PREF_CHANGED:
|
|
return Object.assign({}, prevState, action.data);
|
|
default:
|
|
return prevState;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* insertPinned - Inserts pinned links in their specified slots
|
|
*
|
|
* @param {array} a list of links
|
|
* @param {array} a list of pinned links
|
|
* @return {array} resulting list of links with pinned links inserted
|
|
*/
|
|
function insertPinned(links, pinned) {
|
|
// Remove any pinned links
|
|
const pinnedUrls = pinned.map(link => link && link.url);
|
|
let newLinks = links.filter(link => link ? !pinnedUrls.includes(link.url) : false);
|
|
newLinks = newLinks.map(link => {
|
|
if (link && link.isPinned) {
|
|
delete link.isPinned;
|
|
delete link.pinIndex;
|
|
}
|
|
return link;
|
|
});
|
|
|
|
// Then insert them in their specified location
|
|
pinned.forEach((val, index) => {
|
|
if (!val) {
|
|
return;
|
|
}
|
|
let link = Object.assign({}, val, { isPinned: true, pinIndex: index });
|
|
if (index > newLinks.length) {
|
|
newLinks[index] = link;
|
|
} else {
|
|
newLinks.splice(index, 0, link);
|
|
}
|
|
});
|
|
|
|
return newLinks;
|
|
}
|
|
|
|
|
|
function TopSites(prevState = INITIAL_STATE.TopSites, action) {
|
|
let hasMatch;
|
|
let newRows;
|
|
switch (action.type) {
|
|
case Actions["actionTypes"].TOP_SITES_UPDATED:
|
|
if (!action.data || !action.data.links) {
|
|
return prevState;
|
|
}
|
|
return Object.assign({}, prevState, { initialized: true, rows: action.data.links }, action.data.pref ? { pref: action.data.pref } : {});
|
|
case Actions["actionTypes"].TOP_SITES_PREFS_UPDATED:
|
|
return Object.assign({}, prevState, { pref: action.data.pref });
|
|
case Actions["actionTypes"].TOP_SITES_EDIT:
|
|
return Object.assign({}, prevState, {
|
|
editForm: {
|
|
index: action.data.index,
|
|
previewResponse: null
|
|
}
|
|
});
|
|
case Actions["actionTypes"].TOP_SITES_CANCEL_EDIT:
|
|
return Object.assign({}, prevState, { editForm: null });
|
|
case Actions["actionTypes"].TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL:
|
|
return Object.assign({}, prevState, { showSearchShortcutsForm: true });
|
|
case Actions["actionTypes"].TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL:
|
|
return Object.assign({}, prevState, { showSearchShortcutsForm: false });
|
|
case Actions["actionTypes"].PREVIEW_RESPONSE:
|
|
if (!prevState.editForm || action.data.url !== prevState.editForm.previewUrl) {
|
|
return prevState;
|
|
}
|
|
return Object.assign({}, prevState, {
|
|
editForm: {
|
|
index: prevState.editForm.index,
|
|
previewResponse: action.data.preview,
|
|
previewUrl: action.data.url
|
|
}
|
|
});
|
|
case Actions["actionTypes"].PREVIEW_REQUEST:
|
|
if (!prevState.editForm) {
|
|
return prevState;
|
|
}
|
|
return Object.assign({}, prevState, {
|
|
editForm: {
|
|
index: prevState.editForm.index,
|
|
previewResponse: null,
|
|
previewUrl: action.data.url
|
|
}
|
|
});
|
|
case Actions["actionTypes"].PREVIEW_REQUEST_CANCEL:
|
|
if (!prevState.editForm) {
|
|
return prevState;
|
|
}
|
|
return Object.assign({}, prevState, {
|
|
editForm: {
|
|
index: prevState.editForm.index,
|
|
previewResponse: null
|
|
}
|
|
});
|
|
case Actions["actionTypes"].SCREENSHOT_UPDATED:
|
|
newRows = prevState.rows.map(row => {
|
|
if (row && row.url === action.data.url) {
|
|
hasMatch = true;
|
|
return Object.assign({}, row, { screenshot: action.data.screenshot });
|
|
}
|
|
return row;
|
|
});
|
|
return hasMatch ? Object.assign({}, prevState, { rows: newRows }) : prevState;
|
|
case Actions["actionTypes"].PLACES_BOOKMARK_ADDED:
|
|
if (!action.data) {
|
|
return prevState;
|
|
}
|
|
newRows = prevState.rows.map(site => {
|
|
if (site && site.url === action.data.url) {
|
|
const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data;
|
|
return Object.assign({}, site, { bookmarkGuid, bookmarkTitle, bookmarkDateCreated: dateAdded });
|
|
}
|
|
return site;
|
|
});
|
|
return Object.assign({}, prevState, { rows: newRows });
|
|
case Actions["actionTypes"].PLACES_BOOKMARK_REMOVED:
|
|
if (!action.data) {
|
|
return prevState;
|
|
}
|
|
newRows = prevState.rows.map(site => {
|
|
if (site && site.url === action.data.url) {
|
|
const newSite = Object.assign({}, site);
|
|
delete newSite.bookmarkGuid;
|
|
delete newSite.bookmarkTitle;
|
|
delete newSite.bookmarkDateCreated;
|
|
return newSite;
|
|
}
|
|
return site;
|
|
});
|
|
return Object.assign({}, prevState, { rows: newRows });
|
|
case Actions["actionTypes"].PLACES_LINK_DELETED:
|
|
if (!action.data) {
|
|
return prevState;
|
|
}
|
|
newRows = prevState.rows.filter(site => action.data.url !== site.url);
|
|
return Object.assign({}, prevState, { rows: newRows });
|
|
case Actions["actionTypes"].UPDATE_SEARCH_SHORTCUTS:
|
|
return Object.assign({}, prevState, { searchShortcuts: action.data.searchShortcuts });
|
|
case Actions["actionTypes"].SNIPPETS_PREVIEW_MODE:
|
|
return Object.assign({}, prevState, { rows: [] });
|
|
default:
|
|
return prevState;
|
|
}
|
|
}
|
|
|
|
function Dialog(prevState = INITIAL_STATE.Dialog, action) {
|
|
switch (action.type) {
|
|
case Actions["actionTypes"].DIALOG_OPEN:
|
|
return Object.assign({}, prevState, { visible: true, data: action.data });
|
|
case Actions["actionTypes"].DIALOG_CANCEL:
|
|
return Object.assign({}, prevState, { visible: false });
|
|
case Actions["actionTypes"].DELETE_HISTORY_URL:
|
|
return Object.assign({}, INITIAL_STATE.Dialog);
|
|
default:
|
|
return prevState;
|
|
}
|
|
}
|
|
|
|
function Prefs(prevState = INITIAL_STATE.Prefs, action) {
|
|
let newValues;
|
|
switch (action.type) {
|
|
case Actions["actionTypes"].PREFS_INITIAL_VALUES:
|
|
return Object.assign({}, prevState, { initialized: true, values: action.data });
|
|
case Actions["actionTypes"].PREF_CHANGED:
|
|
newValues = Object.assign({}, prevState.values);
|
|
newValues[action.data.name] = action.data.value;
|
|
return Object.assign({}, prevState, { values: newValues });
|
|
default:
|
|
return prevState;
|
|
}
|
|
}
|
|
|
|
function Sections(prevState = INITIAL_STATE.Sections, action) {
|
|
let hasMatch;
|
|
let newState;
|
|
switch (action.type) {
|
|
case Actions["actionTypes"].SECTION_DEREGISTER:
|
|
return prevState.filter(section => section.id !== action.data);
|
|
case Actions["actionTypes"].SECTION_REGISTER:
|
|
// If section exists in prevState, update it
|
|
newState = prevState.map(section => {
|
|
if (section && section.id === action.data.id) {
|
|
hasMatch = true;
|
|
return Object.assign({}, section, action.data);
|
|
}
|
|
return section;
|
|
});
|
|
// Otherwise, append it
|
|
if (!hasMatch) {
|
|
const initialized = !!(action.data.rows && action.data.rows.length > 0);
|
|
const section = Object.assign({ title: "", rows: [], enabled: false }, action.data, { initialized });
|
|
newState.push(section);
|
|
}
|
|
return newState;
|
|
case Actions["actionTypes"].SECTION_UPDATE:
|
|
newState = prevState.map(section => {
|
|
if (section && section.id === action.data.id) {
|
|
// If the action is updating rows, we should consider initialized to be true.
|
|
// This can be overridden if initialized is defined in the action.data
|
|
const initialized = action.data.rows ? { initialized: true } : {};
|
|
|
|
// Make sure pinned cards stay at their current position when rows are updated.
|
|
// Disabling a section (SECTION_UPDATE with empty rows) does not retain pinned cards.
|
|
if (action.data.rows && action.data.rows.length > 0 && section.rows.find(card => card.pinned)) {
|
|
const rows = Array.from(action.data.rows);
|
|
section.rows.forEach((card, index) => {
|
|
if (card.pinned) {
|
|
// Only add it if it's not already there.
|
|
if (rows[index].guid !== card.guid) {
|
|
rows.splice(index, 0, card);
|
|
}
|
|
}
|
|
});
|
|
return Object.assign({}, section, initialized, Object.assign({}, action.data, { rows }));
|
|
}
|
|
|
|
return Object.assign({}, section, initialized, action.data);
|
|
}
|
|
return section;
|
|
});
|
|
|
|
if (!action.data.dedupeConfigurations) {
|
|
return newState;
|
|
}
|
|
|
|
action.data.dedupeConfigurations.forEach(dedupeConf => {
|
|
newState = newState.map(section => {
|
|
if (section.id === dedupeConf.id) {
|
|
const dedupedRows = dedupeConf.dedupeFrom.reduce((rows, dedupeSectionId) => {
|
|
const dedupeSection = newState.find(s => s.id === dedupeSectionId);
|
|
const [, newRows] = dedupe.group(dedupeSection.rows, rows);
|
|
return newRows;
|
|
}, section.rows);
|
|
|
|
return Object.assign({}, section, { rows: dedupedRows });
|
|
}
|
|
|
|
return section;
|
|
});
|
|
});
|
|
|
|
return newState;
|
|
case Actions["actionTypes"].SECTION_UPDATE_CARD:
|
|
return prevState.map(section => {
|
|
if (section && section.id === action.data.id && section.rows) {
|
|
const newRows = section.rows.map(card => {
|
|
if (card.url === action.data.url) {
|
|
return Object.assign({}, card, action.data.options);
|
|
}
|
|
return card;
|
|
});
|
|
return Object.assign({}, section, { rows: newRows });
|
|
}
|
|
return section;
|
|
});
|
|
case Actions["actionTypes"].PLACES_BOOKMARK_ADDED:
|
|
if (!action.data) {
|
|
return prevState;
|
|
}
|
|
return prevState.map(section => Object.assign({}, section, {
|
|
rows: section.rows.map(item => {
|
|
// find the item within the rows that is attempted to be bookmarked
|
|
if (item.url === action.data.url) {
|
|
const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data;
|
|
return Object.assign({}, item, {
|
|
bookmarkGuid,
|
|
bookmarkTitle,
|
|
bookmarkDateCreated: dateAdded,
|
|
type: "bookmark"
|
|
});
|
|
}
|
|
return item;
|
|
})
|
|
}));
|
|
case Actions["actionTypes"].PLACES_SAVED_TO_POCKET:
|
|
if (!action.data) {
|
|
return prevState;
|
|
}
|
|
return prevState.map(section => Object.assign({}, section, {
|
|
rows: section.rows.map(item => {
|
|
if (item.url === action.data.url) {
|
|
return Object.assign({}, item, {
|
|
open_url: action.data.open_url,
|
|
pocket_id: action.data.pocket_id,
|
|
title: action.data.title,
|
|
type: "pocket"
|
|
});
|
|
}
|
|
return item;
|
|
})
|
|
}));
|
|
case Actions["actionTypes"].PLACES_BOOKMARK_REMOVED:
|
|
if (!action.data) {
|
|
return prevState;
|
|
}
|
|
return prevState.map(section => Object.assign({}, section, {
|
|
rows: section.rows.map(item => {
|
|
// find the bookmark within the rows that is attempted to be removed
|
|
if (item.url === action.data.url) {
|
|
const newSite = Object.assign({}, item);
|
|
delete newSite.bookmarkGuid;
|
|
delete newSite.bookmarkTitle;
|
|
delete newSite.bookmarkDateCreated;
|
|
if (!newSite.type || newSite.type === "bookmark") {
|
|
newSite.type = "history";
|
|
}
|
|
return newSite;
|
|
}
|
|
return item;
|
|
})
|
|
}));
|
|
case Actions["actionTypes"].PLACES_LINK_DELETED:
|
|
case Actions["actionTypes"].PLACES_LINK_BLOCKED:
|
|
if (!action.data) {
|
|
return prevState;
|
|
}
|
|
return prevState.map(section => Object.assign({}, section, { rows: section.rows.filter(site => site.url !== action.data.url) }));
|
|
case Actions["actionTypes"].DELETE_FROM_POCKET:
|
|
case Actions["actionTypes"].ARCHIVE_FROM_POCKET:
|
|
return prevState.map(section => Object.assign({}, section, { rows: section.rows.filter(site => site.pocket_id !== action.data.pocket_id) }));
|
|
case Actions["actionTypes"].SNIPPETS_PREVIEW_MODE:
|
|
return prevState.map(section => Object.assign({}, section, { rows: [] }));
|
|
default:
|
|
return prevState;
|
|
}
|
|
}
|
|
|
|
function Snippets(prevState = INITIAL_STATE.Snippets, action) {
|
|
switch (action.type) {
|
|
case Actions["actionTypes"].SNIPPETS_DATA:
|
|
return Object.assign({}, prevState, { initialized: true }, action.data);
|
|
case Actions["actionTypes"].SNIPPET_BLOCKED:
|
|
return Object.assign({}, prevState, { blockList: prevState.blockList.concat(action.data) });
|
|
case Actions["actionTypes"].SNIPPETS_BLOCKLIST_CLEARED:
|
|
return Object.assign({}, prevState, { blockList: [] });
|
|
case Actions["actionTypes"].SNIPPETS_RESET:
|
|
return INITIAL_STATE.Snippets;
|
|
default:
|
|
return prevState;
|
|
}
|
|
}
|
|
|
|
function Pocket(prevState = INITIAL_STATE.Pocket, action) {
|
|
switch (action.type) {
|
|
case Actions["actionTypes"].POCKET_WAITING_FOR_SPOC:
|
|
return Object.assign({}, prevState, { waitingForSpoc: action.data });
|
|
case Actions["actionTypes"].POCKET_LOGGED_IN:
|
|
return Object.assign({}, prevState, { isUserLoggedIn: !!action.data });
|
|
case Actions["actionTypes"].POCKET_CTA:
|
|
return Object.assign({}, prevState, {
|
|
pocketCta: {
|
|
ctaButton: action.data.cta_button,
|
|
ctaText: action.data.cta_text,
|
|
ctaUrl: action.data.cta_url,
|
|
useCta: action.data.use_cta
|
|
}
|
|
});
|
|
default:
|
|
return prevState;
|
|
}
|
|
}
|
|
|
|
var reducers = { TopSites, App, ASRouter, Snippets, Prefs, Dialog, Sections, Pocket };
|
|
|
|
/***/ }),
|
|
/* 46 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
|
|
// EXTERNAL MODULE: external "React"
|
|
var external_React_ = __webpack_require__(9);
|
|
var external_React_default = /*#__PURE__*/__webpack_require__.n(external_React_);
|
|
|
|
// CONCATENATED MODULE: ./content-src/asrouter/components/ModalOverlay/ModalOverlay.jsx
|
|
|
|
|
|
class ModalOverlay_ModalOverlay extends external_React_default.a.PureComponent {
|
|
componentWillMount() {
|
|
this.setState({ active: true });
|
|
document.body.classList.add("modal-open");
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
document.body.classList.remove("modal-open");
|
|
this.setState({ active: false });
|
|
}
|
|
|
|
render() {
|
|
const { active } = this.state;
|
|
const { title, button_label } = this.props;
|
|
return external_React_default.a.createElement(
|
|
"div",
|
|
null,
|
|
external_React_default.a.createElement("div", { className: `modalOverlayOuter ${active ? "active" : ""}` }),
|
|
external_React_default.a.createElement(
|
|
"div",
|
|
{ className: `modalOverlayInner ${active ? "active" : ""}` },
|
|
external_React_default.a.createElement(
|
|
"h2",
|
|
null,
|
|
" ",
|
|
title,
|
|
" "
|
|
),
|
|
this.props.children,
|
|
external_React_default.a.createElement(
|
|
"div",
|
|
{ className: "footer" },
|
|
external_React_default.a.createElement(
|
|
"button",
|
|
{ tabIndex: "2", onClick: this.props.onDoneButton, className: "button primary modalButton" },
|
|
" ",
|
|
button_label,
|
|
" "
|
|
)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
// CONCATENATED MODULE: ./content-src/asrouter/templates/OnboardingMessage/OnboardingMessage.jsx
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "OnboardingMessage", function() { return OnboardingMessage_OnboardingMessage; });
|
|
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
|
|
|
|
|
|
|
|
|
|
class OnboardingMessage_OnboardingCard extends external_React_default.a.PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
this.onClick = this.onClick.bind(this);
|
|
}
|
|
|
|
onClick() {
|
|
const { props } = this;
|
|
const ping = {
|
|
event: "CLICK_BUTTON",
|
|
message_id: props.id,
|
|
id: props.UISurface
|
|
};
|
|
props.sendUserActionTelemetry(ping);
|
|
props.onAction(props.content.button_action);
|
|
}
|
|
|
|
render() {
|
|
const { content } = this.props;
|
|
return external_React_default.a.createElement(
|
|
"div",
|
|
{ className: "onboardingMessage" },
|
|
external_React_default.a.createElement("div", { className: `onboardingMessageImage ${content.icon}` }),
|
|
external_React_default.a.createElement(
|
|
"div",
|
|
{ className: "onboardingContent" },
|
|
external_React_default.a.createElement(
|
|
"span",
|
|
null,
|
|
external_React_default.a.createElement(
|
|
"h3",
|
|
null,
|
|
" ",
|
|
content.title,
|
|
" "
|
|
),
|
|
external_React_default.a.createElement(
|
|
"p",
|
|
null,
|
|
" ",
|
|
content.text,
|
|
" "
|
|
)
|
|
),
|
|
external_React_default.a.createElement(
|
|
"span",
|
|
null,
|
|
external_React_default.a.createElement(
|
|
"button",
|
|
{ tabIndex: "1", className: "button onboardingButton", onClick: this.onClick },
|
|
" ",
|
|
content.button_label,
|
|
" "
|
|
)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
class OnboardingMessage_OnboardingMessage extends external_React_default.a.PureComponent {
|
|
render() {
|
|
const { props } = this;
|
|
const { button_label, header } = props.extraTemplateStrings;
|
|
return external_React_default.a.createElement(
|
|
ModalOverlay_ModalOverlay,
|
|
_extends({}, props, { button_label: button_label, title: header }),
|
|
external_React_default.a.createElement(
|
|
"div",
|
|
{ className: "onboardingMessageContainer" },
|
|
props.bundle.map(message => external_React_default.a.createElement(OnboardingMessage_OnboardingCard, _extends({ key: message.id,
|
|
sendUserActionTelemetry: props.sendUserActionTelemetry,
|
|
onAction: props.onAction,
|
|
UISurface: props.UISurface
|
|
}, message)))
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
/***/ }),
|
|
/* 47 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
|
|
// EXTERNAL MODULE: ./common/Actions.jsm
|
|
var Actions = __webpack_require__(2);
|
|
|
|
// EXTERNAL MODULE: external "ReactIntl"
|
|
var external_ReactIntl_ = __webpack_require__(13);
|
|
|
|
// CONCATENATED MODULE: ./content-src/components/Card/types.js
|
|
const cardContextTypes = {
|
|
history: {
|
|
intlID: "type_label_visited",
|
|
icon: "history-item"
|
|
},
|
|
bookmark: {
|
|
intlID: "type_label_bookmarked",
|
|
icon: "bookmark-added"
|
|
},
|
|
trending: {
|
|
intlID: "type_label_recommended",
|
|
icon: "trending"
|
|
},
|
|
now: {
|
|
intlID: "type_label_now",
|
|
icon: "now"
|
|
},
|
|
pocket: {
|
|
intlID: "type_label_pocket",
|
|
icon: "pocket"
|
|
},
|
|
download: {
|
|
intlID: "type_label_downloaded",
|
|
icon: "download"
|
|
}
|
|
};
|
|
// EXTERNAL MODULE: external "ReactRedux"
|
|
var external_ReactRedux_ = __webpack_require__(16);
|
|
|
|
// EXTERNAL MODULE: ./content-src/lib/link-menu-options.js
|
|
var link_menu_options = __webpack_require__(23);
|
|
|
|
// EXTERNAL MODULE: ./content-src/components/LinkMenu/LinkMenu.jsx
|
|
var LinkMenu = __webpack_require__(24);
|
|
|
|
// EXTERNAL MODULE: external "React"
|
|
var external_React_ = __webpack_require__(9);
|
|
var external_React_default = /*#__PURE__*/__webpack_require__.n(external_React_);
|
|
|
|
// EXTERNAL MODULE: ./content-src/lib/screenshot-utils.js
|
|
var screenshot_utils = __webpack_require__(26);
|
|
|
|
// CONCATENATED MODULE: ./content-src/components/Card/Card.jsx
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_Card", function() { return Card_Card; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Card", function() { return Card; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "PlaceholderCard", function() { return PlaceholderCard; });
|
|
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Keep track of pending image loads to only request once
|
|
const gImageLoading = new Map();
|
|
|
|
/**
|
|
* Card component.
|
|
* Cards are found within a Section component and contain information about a link such
|
|
* as preview image, page title, page description, and some context about if the page
|
|
* was visited, bookmarked, trending etc...
|
|
* Each Section can make an unordered list of Cards which will create one instane of
|
|
* this class. Each card will then get a context menu which reflects the actions that
|
|
* can be done on this Card.
|
|
*/
|
|
class Card_Card extends external_React_default.a.PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = {
|
|
activeCard: null,
|
|
imageLoaded: false,
|
|
showContextMenu: false,
|
|
cardImage: null
|
|
};
|
|
this.onMenuButtonClick = this.onMenuButtonClick.bind(this);
|
|
this.onMenuUpdate = this.onMenuUpdate.bind(this);
|
|
this.onLinkClick = this.onLinkClick.bind(this);
|
|
}
|
|
|
|
/**
|
|
* Helper to conditionally load an image and update state when it loads.
|
|
*/
|
|
maybeLoadImage() {
|
|
var _this = this;
|
|
|
|
return _asyncToGenerator(function* () {
|
|
// No need to load if it's already loaded or no image
|
|
const { cardImage } = _this.state;
|
|
if (!cardImage) {
|
|
return;
|
|
}
|
|
|
|
const imageUrl = cardImage.url;
|
|
if (!_this.state.imageLoaded) {
|
|
// Initialize a promise to share a load across multiple card updates
|
|
if (!gImageLoading.has(imageUrl)) {
|
|
const loaderPromise = new Promise(function (resolve, reject) {
|
|
const loader = new Image();
|
|
loader.addEventListener("load", resolve);
|
|
loader.addEventListener("error", reject);
|
|
loader.src = imageUrl;
|
|
});
|
|
|
|
// Save and remove the promise only while it's pending
|
|
gImageLoading.set(imageUrl, loaderPromise);
|
|
loaderPromise.catch(function (ex) {
|
|
return ex;
|
|
}).then(function () {
|
|
return gImageLoading.delete(imageUrl);
|
|
}).catch();
|
|
}
|
|
|
|
// Wait for the image whether just started loading or reused promise
|
|
yield gImageLoading.get(imageUrl);
|
|
|
|
// Only update state if we're still waiting to load the original image
|
|
if (screenshot_utils["ScreenshotUtils"].isRemoteImageLocal(_this.state.cardImage, _this.props.link.image) && !_this.state.imageLoaded) {
|
|
_this.setState({ imageLoaded: true });
|
|
}
|
|
}
|
|
})();
|
|
}
|
|
|
|
/**
|
|
* Helper to obtain the next state based on nextProps and prevState.
|
|
*
|
|
* NOTE: Rename this method to getDerivedStateFromProps when we update React
|
|
* to >= 16.3. We will need to update tests as well. We cannot rename this
|
|
* method to getDerivedStateFromProps now because there is a mismatch in
|
|
* the React version that we are using for both testing and production.
|
|
* (i.e. react-test-render => "16.3.2", react => "16.2.0").
|
|
*
|
|
* See https://github.com/airbnb/enzyme/blob/master/packages/enzyme-adapter-react-16/package.json#L43.
|
|
*/
|
|
static getNextStateFromProps(nextProps, prevState) {
|
|
const { image } = nextProps.link;
|
|
const imageInState = screenshot_utils["ScreenshotUtils"].isRemoteImageLocal(prevState.cardImage, image);
|
|
let nextState = null;
|
|
|
|
// Image is updating.
|
|
if (!imageInState && nextProps.link) {
|
|
nextState = { imageLoaded: false };
|
|
}
|
|
|
|
if (imageInState) {
|
|
return nextState;
|
|
}
|
|
|
|
// Since image was updated, attempt to revoke old image blob URL, if it exists.
|
|
screenshot_utils["ScreenshotUtils"].maybeRevokeBlobObjectURL(prevState.cardImage);
|
|
|
|
nextState = nextState || {};
|
|
nextState.cardImage = screenshot_utils["ScreenshotUtils"].createLocalImageObject(image);
|
|
|
|
return nextState;
|
|
}
|
|
|
|
onMenuButtonClick(event) {
|
|
event.preventDefault();
|
|
this.setState({
|
|
activeCard: this.props.index,
|
|
showContextMenu: true
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Report to telemetry additional information about the item.
|
|
*/
|
|
_getTelemetryInfo() {
|
|
// Filter out "history" type for being the default
|
|
if (this.props.link.type !== "history") {
|
|
return { value: { card_type: this.props.link.type } };
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
onLinkClick(event) {
|
|
event.preventDefault();
|
|
if (this.props.link.type === "download") {
|
|
this.props.dispatch(Actions["actionCreators"].OnlyToMain({
|
|
type: Actions["actionTypes"].SHOW_DOWNLOAD_FILE,
|
|
data: this.props.link
|
|
}));
|
|
} else {
|
|
const { altKey, button, ctrlKey, metaKey, shiftKey } = event;
|
|
this.props.dispatch(Actions["actionCreators"].OnlyToMain({
|
|
type: Actions["actionTypes"].OPEN_LINK,
|
|
data: Object.assign(this.props.link, { event: { altKey, button, ctrlKey, metaKey, shiftKey } })
|
|
}));
|
|
}
|
|
if (this.props.isWebExtension) {
|
|
this.props.dispatch(Actions["actionCreators"].WebExtEvent(Actions["actionTypes"].WEBEXT_CLICK, {
|
|
source: this.props.eventSource,
|
|
url: this.props.link.url,
|
|
action_position: this.props.index
|
|
}));
|
|
} else {
|
|
this.props.dispatch(Actions["actionCreators"].UserEvent(Object.assign({
|
|
event: "CLICK",
|
|
source: this.props.eventSource,
|
|
action_position: this.props.index
|
|
}, this._getTelemetryInfo())));
|
|
|
|
if (this.props.shouldSendImpressionStats) {
|
|
this.props.dispatch(Actions["actionCreators"].ImpressionStats({
|
|
source: this.props.eventSource,
|
|
click: 0,
|
|
tiles: [{ id: this.props.link.guid, pos: this.props.index }]
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
|
|
onMenuUpdate(showContextMenu) {
|
|
this.setState({ showContextMenu });
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.maybeLoadImage();
|
|
}
|
|
|
|
componentDidUpdate() {
|
|
this.maybeLoadImage();
|
|
}
|
|
|
|
// NOTE: Remove this function when we update React to >= 16.3 since React will
|
|
// call getDerivedStateFromProps automatically. We will also need to
|
|
// rename getNextStateFromProps to getDerivedStateFromProps.
|
|
componentWillMount() {
|
|
const nextState = Card_Card.getNextStateFromProps(this.props, this.state);
|
|
if (nextState) {
|
|
this.setState(nextState);
|
|
}
|
|
}
|
|
|
|
// NOTE: Remove this function when we update React to >= 16.3 since React will
|
|
// call getDerivedStateFromProps automatically. We will also need to
|
|
// rename getNextStateFromProps to getDerivedStateFromProps.
|
|
componentWillReceiveProps(nextProps) {
|
|
const nextState = Card_Card.getNextStateFromProps(nextProps, this.state);
|
|
if (nextState) {
|
|
this.setState(nextState);
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
screenshot_utils["ScreenshotUtils"].maybeRevokeBlobObjectURL(this.state.cardImage);
|
|
}
|
|
|
|
render() {
|
|
const { index, className, link, dispatch, contextMenuOptions, eventSource, shouldSendImpressionStats } = this.props;
|
|
const { props } = this;
|
|
const isContextMenuOpen = this.state.showContextMenu && this.state.activeCard === index;
|
|
// Display "now" as "trending" until we have new strings #3402
|
|
const { icon, intlID } = cardContextTypes[link.type === "now" ? "trending" : link.type] || {};
|
|
const hasImage = this.state.cardImage || link.hasImage;
|
|
const imageStyle = { backgroundImage: this.state.cardImage ? `url(${this.state.cardImage.url})` : "none" };
|
|
const outerClassName = ["card-outer", className, isContextMenuOpen && "active", props.placeholder && "placeholder"].filter(v => v).join(" ");
|
|
|
|
return external_React_default.a.createElement(
|
|
"li",
|
|
{ className: outerClassName },
|
|
external_React_default.a.createElement(
|
|
"a",
|
|
{ href: link.type === "pocket" ? link.open_url : link.url, onClick: !props.placeholder ? this.onLinkClick : undefined },
|
|
external_React_default.a.createElement(
|
|
"div",
|
|
{ className: "card" },
|
|
external_React_default.a.createElement(
|
|
"div",
|
|
{ className: "card-preview-image-outer" },
|
|
hasImage && external_React_default.a.createElement("div", { className: `card-preview-image${this.state.imageLoaded ? " loaded" : ""}`, style: imageStyle })
|
|
),
|
|
external_React_default.a.createElement(
|
|
"div",
|
|
{ className: "card-details" },
|
|
link.type === "download" && external_React_default.a.createElement(
|
|
"div",
|
|
{ className: "card-host-name alternate" },
|
|
external_React_default.a.createElement(external_ReactIntl_["FormattedMessage"], { id: Object(link_menu_options["GetPlatformString"])(this.props.platform) })
|
|
),
|
|
link.hostname && external_React_default.a.createElement(
|
|
"div",
|
|
{ className: "card-host-name" },
|
|
link.hostname.slice(0, 100),
|
|
link.type === "download" && ` \u2014 ${link.description}`
|
|
),
|
|
external_React_default.a.createElement(
|
|
"div",
|
|
{ className: ["card-text", icon ? "" : "no-context", link.description ? "" : "no-description", link.hostname ? "" : "no-host-name"].join(" ") },
|
|
external_React_default.a.createElement(
|
|
"h4",
|
|
{ className: "card-title", dir: "auto" },
|
|
link.title
|
|
),
|
|
external_React_default.a.createElement(
|
|
"p",
|
|
{ className: "card-description", dir: "auto" },
|
|
link.description
|
|
)
|
|
),
|
|
external_React_default.a.createElement(
|
|
"div",
|
|
{ className: "card-context" },
|
|
icon && !link.context && external_React_default.a.createElement("span", { className: `card-context-icon icon icon-${icon}` }),
|
|
link.icon && link.context && external_React_default.a.createElement("span", { className: "card-context-icon icon", style: { backgroundImage: `url('${link.icon}')` } }),
|
|
intlID && !link.context && external_React_default.a.createElement(
|
|
"div",
|
|
{ className: "card-context-label" },
|
|
external_React_default.a.createElement(external_ReactIntl_["FormattedMessage"], { id: intlID, defaultMessage: "Visited" })
|
|
),
|
|
link.context && external_React_default.a.createElement(
|
|
"div",
|
|
{ className: "card-context-label" },
|
|
link.context
|
|
)
|
|
)
|
|
)
|
|
)
|
|
),
|
|
!props.placeholder && external_React_default.a.createElement(
|
|
"button",
|
|
{ className: "context-menu-button icon", title: this.props.intl.formatMessage({ id: "context_menu_title" }),
|
|
onClick: this.onMenuButtonClick },
|
|
external_React_default.a.createElement(
|
|
"span",
|
|
{ className: "sr-only" },
|
|
`Open context menu for ${link.title}`
|
|
)
|
|
),
|
|
isContextMenuOpen && external_React_default.a.createElement(LinkMenu["LinkMenu"], {
|
|
dispatch: dispatch,
|
|
index: index,
|
|
source: eventSource,
|
|
onUpdate: this.onMenuUpdate,
|
|
options: link.contextMenuOptions || contextMenuOptions,
|
|
site: link,
|
|
siteInfo: this._getTelemetryInfo(),
|
|
shouldSendImpressionStats: shouldSendImpressionStats })
|
|
);
|
|
}
|
|
}
|
|
Card_Card.defaultProps = { link: {} };
|
|
const Card = Object(external_ReactRedux_["connect"])(state => ({ platform: state.Prefs.values.platform }))(Object(external_ReactIntl_["injectIntl"])(Card_Card));
|
|
const PlaceholderCard = props => external_React_default.a.createElement(Card, { placeholder: true, className: props.className });
|
|
|
|
/***/ }),
|
|
/* 48 */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
|
|
// EXTERNAL MODULE: ./common/Actions.jsm
|
|
var Actions = __webpack_require__(2);
|
|
|
|
// EXTERNAL MODULE: external "ReactIntl"
|
|
var external_ReactIntl_ = __webpack_require__(13);
|
|
|
|
// EXTERNAL MODULE: external "React"
|
|
var external_React_ = __webpack_require__(9);
|
|
var external_React_default = /*#__PURE__*/__webpack_require__.n(external_React_);
|
|
|
|
// EXTERNAL MODULE: ./content-src/components/TopSites/TopSitesConstants.js
|
|
var TopSitesConstants = __webpack_require__(36);
|
|
|
|
// CONCATENATED MODULE: ./content-src/components/TopSites/TopSiteFormInput.jsx
|
|
|
|
|
|
|
|
class TopSiteFormInput_TopSiteFormInput extends external_React_default.a.PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = { validationError: this.props.validationError };
|
|
this.onChange = this.onChange.bind(this);
|
|
this.onMount = this.onMount.bind(this);
|
|
}
|
|
|
|
componentWillReceiveProps(nextProps) {
|
|
if (nextProps.shouldFocus && !this.props.shouldFocus) {
|
|
this.input.focus();
|
|
}
|
|
if (nextProps.validationError && !this.props.validationError) {
|
|
this.setState({ validationError: true });
|
|
}
|
|
// If the component is in an error state but the value was cleared by the parent
|
|
if (this.state.validationError && !nextProps.value) {
|
|
this.setState({ validationError: false });
|
|
}
|
|
}
|
|
|
|
onChange(ev) {
|
|
if (this.state.validationError) {
|
|
this.setState({ validationError: false });
|
|
}
|
|
this.props.onChange(ev);
|
|
}
|
|
|
|
onMount(input) {
|
|
this.input = input;
|
|
}
|
|
|
|
render() {
|
|
const showClearButton = this.props.value && this.props.onClear;
|
|
const { typeUrl } = this.props;
|
|
const { validationError } = this.state;
|
|
|
|
return external_React_default.a.createElement(
|
|
"label",
|
|
null,
|
|
external_React_default.a.createElement(external_ReactIntl_["FormattedMessage"], { id: this.props.titleId }),
|
|
external_React_default.a.createElement(
|
|
"div",
|
|
{ className: `field ${typeUrl ? "url" : ""}${validationError ? " invalid" : ""}` },
|
|
this.props.loading ? external_React_default.a.createElement(
|
|
"div",
|
|
{ className: "loading-container" },
|
|
external_React_default.a.createElement("div", { className: "loading-animation" })
|
|
) : showClearButton && external_React_default.a.createElement("div", { className: "icon icon-clear-input", onClick: this.props.onClear }),
|
|
external_React_default.a.createElement("input", { type: "text",
|
|
value: this.props.value,
|
|
ref: this.onMount,
|
|
onChange: this.onChange,
|
|
placeholder: this.props.intl.formatMessage({ id: this.props.placeholderId }),
|
|
autoFocus: this.props.shouldFocus,
|
|
disabled: this.props.loading }),
|
|
validationError && external_React_default.a.createElement(
|
|
"aside",
|
|
{ className: "error-tooltip" },
|
|
external_React_default.a.createElement(external_ReactIntl_["FormattedMessage"], { id: this.props.errorMessageId })
|
|
)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
TopSiteFormInput_TopSiteFormInput.defaultProps = {
|
|
showClearButton: false,
|
|
value: "",
|
|
validationError: false
|
|
};
|
|
// EXTERNAL MODULE: ./content-src/components/TopSites/TopSite.jsx
|
|
var TopSite = __webpack_require__(38);
|
|
|
|
// CONCATENATED MODULE: ./content-src/components/TopSites/TopSiteForm.jsx
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TopSiteForm", function() { return TopSiteForm_TopSiteForm; });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TopSiteForm_TopSiteForm extends external_React_default.a.PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
const { site } = props;
|
|
this.state = {
|
|
label: site ? site.label || site.hostname : "",
|
|
url: site ? site.url : "",
|
|
validationError: false,
|
|
customScreenshotUrl: site ? site.customScreenshotURL : "",
|
|
showCustomScreenshotForm: site ? site.customScreenshotURL : false
|
|
};
|
|
this.onClearScreenshotInput = this.onClearScreenshotInput.bind(this);
|
|
this.onLabelChange = this.onLabelChange.bind(this);
|
|
this.onUrlChange = this.onUrlChange.bind(this);
|
|
this.onCancelButtonClick = this.onCancelButtonClick.bind(this);
|
|
this.onClearUrlClick = this.onClearUrlClick.bind(this);
|
|
this.onDoneButtonClick = this.onDoneButtonClick.bind(this);
|
|
this.onCustomScreenshotUrlChange = this.onCustomScreenshotUrlChange.bind(this);
|
|
this.onPreviewButtonClick = this.onPreviewButtonClick.bind(this);
|
|
this.onEnableScreenshotUrlForm = this.onEnableScreenshotUrlForm.bind(this);
|
|
this.validateUrl = this.validateUrl.bind(this);
|
|
}
|
|
|
|
onLabelChange(event) {
|
|
this.setState({ "label": event.target.value });
|
|
}
|
|
|
|
onUrlChange(event) {
|
|
this.setState({
|
|
url: event.target.value,
|
|
validationError: false
|
|
});
|
|
}
|
|
|
|
onClearUrlClick() {
|
|
this.setState({
|
|
url: "",
|
|
validationError: false
|
|
});
|
|
}
|
|
|
|
onEnableScreenshotUrlForm() {
|
|
this.setState({ showCustomScreenshotForm: true });
|
|
}
|
|
|
|
_updateCustomScreenshotInput(customScreenshotUrl) {
|
|
this.setState({
|
|
customScreenshotUrl,
|
|
validationError: false
|
|
});
|
|
this.props.dispatch({ type: Actions["actionTypes"].PREVIEW_REQUEST_CANCEL });
|
|
}
|
|
|
|
onCustomScreenshotUrlChange(event) {
|
|
this._updateCustomScreenshotInput(event.target.value);
|
|
}
|
|
|
|
onClearScreenshotInput() {
|
|
this._updateCustomScreenshotInput("");
|
|
}
|
|
|
|
onCancelButtonClick(ev) {
|
|
ev.preventDefault();
|
|
this.props.onClose();
|
|
}
|
|
|
|
onDoneButtonClick(ev) {
|
|
ev.preventDefault();
|
|
|
|
if (this.validateForm()) {
|
|
const site = { url: this.cleanUrl(this.state.url) };
|
|
const { index } = this.props;
|
|
if (this.state.label !== "") {
|
|
site.label = this.state.label;
|
|
}
|
|
|
|
if (this.state.customScreenshotUrl) {
|
|
site.customScreenshotURL = this.cleanUrl(this.state.customScreenshotUrl);
|
|
} else if (this.props.site && this.props.site.customScreenshotURL) {
|
|
// Used to flag that previously cached screenshot should be removed
|
|
site.customScreenshotURL = null;
|
|
}
|
|
this.props.dispatch(Actions["actionCreators"].AlsoToMain({
|
|
type: Actions["actionTypes"].TOP_SITES_PIN,
|
|
data: { site, index }
|
|
}));
|
|
this.props.dispatch(Actions["actionCreators"].UserEvent({
|
|
source: TopSitesConstants["TOP_SITES_SOURCE"],
|
|
event: "TOP_SITES_EDIT",
|
|
action_position: index
|
|
}));
|
|
|
|
this.props.onClose();
|
|
}
|
|
}
|
|
|
|
onPreviewButtonClick(event) {
|
|
event.preventDefault();
|
|
if (this.validateForm()) {
|
|
this.props.dispatch(Actions["actionCreators"].AlsoToMain({
|
|
type: Actions["actionTypes"].PREVIEW_REQUEST,
|
|
data: { url: this.cleanUrl(this.state.customScreenshotUrl) }
|
|
}));
|
|
this.props.dispatch(Actions["actionCreators"].UserEvent({
|
|
source: TopSitesConstants["TOP_SITES_SOURCE"],
|
|
event: "PREVIEW_REQUEST"
|
|
}));
|
|
}
|
|
}
|
|
|
|
cleanUrl(url) {
|
|
// If we are missing a protocol, prepend http://
|
|
if (!url.startsWith("http:") && !url.startsWith("https:")) {
|
|
return `http://${url}`;
|
|
}
|
|
return url;
|
|
}
|
|
|
|
_tryParseUrl(url) {
|
|
try {
|
|
return new URL(url);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
validateUrl(url) {
|
|
const validProtocols = ["http:", "https:"];
|
|
const urlObj = this._tryParseUrl(url) || this._tryParseUrl(this.cleanUrl(url));
|
|
|
|
return urlObj && validProtocols.includes(urlObj.protocol);
|
|
}
|
|
|
|
validateCustomScreenshotUrl() {
|
|
const { customScreenshotUrl } = this.state;
|
|
return !customScreenshotUrl || this.validateUrl(customScreenshotUrl);
|
|
}
|
|
|
|
validateForm() {
|
|
const validate = this.validateUrl(this.state.url) && this.validateCustomScreenshotUrl();
|
|
|
|
if (!validate) {
|
|
this.setState({ validationError: true });
|
|
}
|
|
|
|
return validate;
|
|
}
|
|
|
|
_renderCustomScreenshotInput() {
|
|
const { customScreenshotUrl } = this.state;
|
|
const requestFailed = this.props.previewResponse === "";
|
|
const validationError = this.state.validationError && !this.validateCustomScreenshotUrl() || requestFailed;
|
|
// Set focus on error if the url field is valid or when the input is first rendered and is empty
|
|
const shouldFocus = validationError && this.validateUrl(this.state.url) || !customScreenshotUrl;
|
|
const isLoading = this.props.previewResponse === null && customScreenshotUrl && this.props.previewUrl === this.cleanUrl(customScreenshotUrl);
|
|
|
|
if (!this.state.showCustomScreenshotForm) {
|
|
return external_React_default.a.createElement(
|
|
"a",
|
|
{ className: "enable-custom-image-input", onClick: this.onEnableScreenshotUrlForm },
|
|
external_React_default.a.createElement(external_ReactIntl_["FormattedMessage"], { id: "topsites_form_use_image_link" })
|
|
);
|
|
}
|
|
return external_React_default.a.createElement(
|
|
"div",
|
|
{ className: "custom-image-input-container" },
|
|
external_React_default.a.createElement(TopSiteFormInput_TopSiteFormInput, {
|
|
errorMessageId: requestFailed ? "topsites_form_image_validation" : "topsites_form_url_validation",
|
|
loading: isLoading,
|
|
onChange: this.onCustomScreenshotUrlChange,
|
|
onClear: this.onClearScreenshotInput,
|
|
shouldFocus: shouldFocus,
|
|
typeUrl: true,
|
|
value: customScreenshotUrl,
|
|
validationError: validationError,
|
|
titleId: "topsites_form_image_url_label",
|
|
placeholderId: "topsites_form_url_placeholder",
|
|
intl: this.props.intl })
|
|
);
|
|
}
|
|
|
|
render() {
|
|
const { customScreenshotUrl } = this.state;
|
|
const requestFailed = this.props.previewResponse === "";
|
|
// For UI purposes, editing without an existing link is "add"
|
|
const showAsAdd = !this.props.site;
|
|
const previous = this.props.site && this.props.site.customScreenshotURL || "";
|
|
const changed = customScreenshotUrl && this.cleanUrl(customScreenshotUrl) !== previous;
|
|
// Preview mode if changes were made to the custom screenshot URL and no preview was received yet
|
|
// or the request failed
|
|
const previewMode = changed && !this.props.previewResponse;
|
|
const previewLink = Object.assign({}, this.props.site);
|
|
if (this.props.previewResponse) {
|
|
previewLink.screenshot = this.props.previewResponse;
|
|
previewLink.customScreenshotURL = this.props.previewUrl;
|
|
}
|
|
return external_React_default.a.createElement(
|
|
"form",
|
|
{ className: "topsite-form" },
|
|
external_React_default.a.createElement(
|
|
"div",
|
|
{ className: "form-input-container" },
|
|
external_React_default.a.createElement(
|
|
"h3",
|
|
{ className: "section-title" },
|
|
external_React_default.a.createElement(external_ReactIntl_["FormattedMessage"], { id: showAsAdd ? "topsites_form_add_header" : "topsites_form_edit_header" })
|
|
),
|
|
external_React_default.a.createElement(
|
|
"div",
|
|
{ className: "fields-and-preview" },
|
|
external_React_default.a.createElement(
|
|
"div",
|
|
{ className: "form-wrapper" },
|
|
external_React_default.a.createElement(TopSiteFormInput_TopSiteFormInput, { onChange: this.onLabelChange,
|
|
value: this.state.label,
|
|
titleId: "topsites_form_title_label",
|
|
placeholderId: "topsites_form_title_placeholder",
|
|
intl: this.props.intl }),
|
|
external_React_default.a.createElement(TopSiteFormInput_TopSiteFormInput, { onChange: this.onUrlChange,
|
|
shouldFocus: this.state.validationError && !this.validateUrl(this.state.url),
|
|
value: this.state.url,
|
|
onClear: this.onClearUrlClick,
|
|
validationError: this.state.validationError && !this.validateUrl(this.state.url),
|
|
titleId: "topsites_form_url_label",
|
|
typeUrl: true,
|
|
placeholderId: "topsites_form_url_placeholder",
|
|
errorMessageId: "topsites_form_url_validation",
|
|
intl: this.props.intl }),
|
|
this._renderCustomScreenshotInput()
|
|
),
|
|
external_React_default.a.createElement(TopSite["TopSiteLink"], { link: previewLink,
|
|
defaultStyle: requestFailed,
|
|
title: this.state.label })
|
|
)
|
|
),
|
|
external_React_default.a.createElement(
|
|
"section",
|
|
{ className: "actions" },
|
|
external_React_default.a.createElement(
|
|
"button",
|
|
{ className: "cancel", type: "button", onClick: this.onCancelButtonClick },
|
|
external_React_default.a.createElement(external_ReactIntl_["FormattedMessage"], { id: "topsites_form_cancel_button" })
|
|
),
|
|
previewMode ? external_React_default.a.createElement(
|
|
"button",
|
|
{ className: "done preview", type: "submit", onClick: this.onPreviewButtonClick },
|
|
external_React_default.a.createElement(external_ReactIntl_["FormattedMessage"], { id: "topsites_form_preview_button" })
|
|
) : external_React_default.a.createElement(
|
|
"button",
|
|
{ className: "done", type: "submit", onClick: this.onDoneButtonClick },
|
|
external_React_default.a.createElement(external_ReactIntl_["FormattedMessage"], { id: showAsAdd ? "topsites_form_add_button" : "topsites_form_save_button" })
|
|
)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
TopSiteForm_TopSiteForm.defaultProps = {
|
|
site: null,
|
|
index: -1
|
|
};
|
|
|
|
/***/ })
|
|
/******/ ]);
|
|
//# sourceMappingURL=activity-stream.bundle.js.map
|