Merge remote-tracking branch 'origin/master' into beastify-injection-fixes
* origin/master: (26 commits) convert chrome. to browser. Issue #165 #166 (#262) Update examples.json for contextMenus->menus change (#282) Update context-menu-demo (#272) change shortcut to Ctrl+Shift+U for commands example (#264) Update `proxy-blocker` extension to be compatible with firefox 56+ (#260) Update eslint and .travis.yml (#259) add in titlePreface (#256) Hellosct1 webext (#237) Remove 'highlight', as Firefox does not support it (#252) Reflect bookmark state in icon title to make it accessible to screen readers (#255) Adding listing/descriptions for the imagify and themes examples. (#253) Selfify example (#251) New theme examples (#248) Example: embedded webextension overlay (#249) Add an indexedDB file storage example: image-reference-collector (#224) webextension -> extension (#250) more fields into package.json Add travis-ci build status badge Add .travis.yml more .eslintrc.json and fixes ...
4
.eslintignore
Normal file
@@ -0,0 +1,4 @@
|
||||
**/node_modules/**
|
||||
react-es6-popup/**/dist
|
||||
mocha-client-tests
|
||||
store-collected-images/webextension-plain/deps
|
||||
22
.eslintrc.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"root": true,
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"webextensions": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended"
|
||||
],
|
||||
"rules": {
|
||||
"no-console": 0,
|
||||
"no-unused-vars": ["warn", { "vars": "all", "args": "all" } ],
|
||||
"no-undef": ["warn"],
|
||||
"no-proto": ["error"],
|
||||
"prefer-arrow-callback": ["warn"],
|
||||
"prefer-spread": ["warn"]
|
||||
}
|
||||
}
|
||||
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
3
.travis.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
language: node_js
|
||||
node_js: stable
|
||||
sudo: false
|
||||
@@ -1,4 +1,4 @@
|
||||
# webextensions-examples
|
||||
# webextensions-examples [](https://travis-ci.org/mdn/webextensions-examples)
|
||||
|
||||
[https://github.com/mdn/webextensions-examples](https://github.com/mdn/webextensions-examples)
|
||||
|
||||
|
||||
@@ -4,14 +4,14 @@ const contentBox = document.querySelector("#content");
|
||||
/*
|
||||
Make the content box editable as soon as the user mouses over the sidebar.
|
||||
*/
|
||||
window.addEventListener("mouseover", (e) => {
|
||||
window.addEventListener("mouseover", () => {
|
||||
contentBox.setAttribute("contenteditable", true);
|
||||
});
|
||||
|
||||
/*
|
||||
When the user mouses out, save the current contents of the box.
|
||||
*/
|
||||
window.addEventListener("mouseout", (e) => {
|
||||
window.addEventListener("mouseout", () => {
|
||||
contentBox.setAttribute("contenteditable", false);
|
||||
browser.tabs.query({windowId: myWindowId, active: true}).then((tabs) => {
|
||||
let contentToStore = {};
|
||||
|
||||
@@ -51,7 +51,7 @@ When first loaded, initialize the page action for all tabs.
|
||||
*/
|
||||
var gettingAllTabs = browser.tabs.query({});
|
||||
gettingAllTabs.then((tabs) => {
|
||||
for (tab of tabs) {
|
||||
for (let tab of tabs) {
|
||||
initializePageAction(tab);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -16,6 +16,11 @@ function updateIcon() {
|
||||
},
|
||||
tabId: currentTab.id
|
||||
});
|
||||
browser.browserAction.setTitle({
|
||||
// Screen readers can see the title
|
||||
title: currentBookmark ? 'Unbookmark it!' : 'Bookmark it!',
|
||||
tabId: currentTab.id
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -69,6 +69,6 @@ browser.alarms.onAlarm.addListener((alarm) => {
|
||||
/*
|
||||
On page action click, navigate the corresponding tab to the cat gifs.
|
||||
*/
|
||||
browser.pageAction.onClicked.addListener(function () {
|
||||
browser.pageAction.onClicked.addListener(() => {
|
||||
browser.tabs.update({url: CATGIFS});
|
||||
});
|
||||
|
||||
@@ -10,4 +10,4 @@ All it does is:
|
||||
|
||||
It shows:
|
||||
|
||||
* how to use chrome.commands to register keyboard shortcuts for your extension.
|
||||
* how to use browser.commands to register keyboard shortcuts for your extension.
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
*/
|
||||
var gettingAllCommands = browser.commands.getAll();
|
||||
gettingAllCommands.then((commands) => {
|
||||
for (command of commands) {
|
||||
for (let command of commands) {
|
||||
console.log(command);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ browser.contextMenus.create({
|
||||
title: "Copy link to clipboard",
|
||||
contexts: ["link"],
|
||||
});
|
||||
browser.contextMenus.onClicked.addListener(function(info, tab) {
|
||||
browser.contextMenus.onClicked.addListener((info, tab) => {
|
||||
if (info.menuItemId === "copy-link-to-clipboard") {
|
||||
// Examples: text and HTML to be copied.
|
||||
const text = "This is text: " + info.linkUrl;
|
||||
@@ -22,7 +22,7 @@ browser.contextMenus.onClicked.addListener(function(info, tab) {
|
||||
|
||||
browser.tabs.executeScript({
|
||||
code: "typeof copyToClipboard === 'function';",
|
||||
}).then(function(results) {
|
||||
}).then((results) => {
|
||||
// The content script's last expression will be true if the function
|
||||
// has been defined. If this is not the case, then we need to run
|
||||
// clipboard-helper.js to define function copyToClipboard.
|
||||
@@ -31,11 +31,11 @@ browser.contextMenus.onClicked.addListener(function(info, tab) {
|
||||
file: "clipboard-helper.js",
|
||||
});
|
||||
}
|
||||
}).then(function() {
|
||||
}).then(() => {
|
||||
return browser.tabs.executeScript(tab.id, {
|
||||
code,
|
||||
});
|
||||
}).catch(function(error) {
|
||||
}).catch((error) => {
|
||||
// This could happen if the extension is not allowed to run code in
|
||||
// the page, for example if the tab is a privileged page.
|
||||
console.error("Failed to copy text: " + error);
|
||||
|
||||
29
devtools-panels/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# devtools-panels
|
||||
|
||||
**Adds a new panel to the developer tools. The panel contains buttons that demonstrate various basic features of the devtools API.**
|
||||
|
||||
## What it does ##
|
||||
|
||||
This extension adds a new panel to the developer tools. The panel contains four buttons:
|
||||
|
||||
* **Inspect H1**: this injects a script into the active page. The script uses the [`inspect()` helper function](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/devtools.inspectedWindow/eval#Helpers) to select the first <h1> element in the page in the devtools inspector.
|
||||
|
||||
* **Reddinate inspected element**: this injects a script into the active page. The script uses the [`$0` helper](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/devtools.inspectedWindow/eval#Helpers) to get the element that's currently selected in the devtools Inspector, and gives it a red background.
|
||||
|
||||
* **Check for jQuery**: this injects a script into the active page. The script checks whether `jQuery` is defined in the page, and logs a string to the add-on debugging console (note: *not* the web console) recording the result.
|
||||
|
||||
* **Inject content script**: this sends a message to the extension's background script, asking it to inject a given content script in the active page.
|
||||
|
||||
To learn more about the devtools APIs, see [Extending the developer tools](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Extending_the_developer_tools).
|
||||
|
||||
## What it shows ##
|
||||
|
||||
* How to add a new panel to the devtools.
|
||||
|
||||
* How to inject a script into the active page using [`inspectedWindow.eval()`](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/devtools.inspectedWindow/eval).
|
||||
|
||||
* How to use [helpers](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/devtools.inspectedWindow/eval#Helpers) to interact with the devtools.
|
||||
|
||||
* That unlike content scripts, scripts injected with `eval()` can see objects, like `jQuery`, that were added by page scripts.
|
||||
|
||||
* How to send messages to the background script.
|
||||
23
devtools-panels/background_scripts/background.js
Normal file
@@ -0,0 +1,23 @@
|
||||
|
||||
/**
|
||||
When we receive the message, execute the given script in the given
|
||||
tab.
|
||||
*/
|
||||
function handleMessage(request, sender, sendResponse) {
|
||||
|
||||
if (sender.url != browser.runtime.getURL("/devtools/panel/panel.html")) {
|
||||
return;
|
||||
}
|
||||
|
||||
browser.tabs.executeScript(
|
||||
request.tabId,
|
||||
{
|
||||
code: request.script
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
Listen for messages from our devtools panel.
|
||||
*/
|
||||
browser.runtime.onMessage.addListener(handleMessage);
|
||||
9
devtools-panels/devtools/devtools-page.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
<script src="devtools.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
24
devtools-panels/devtools/devtools.js
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
This script is run whenever the devtools are open.
|
||||
In here, we can create our panel.
|
||||
*/
|
||||
|
||||
function handleShown() {
|
||||
console.log("panel is being shown");
|
||||
}
|
||||
|
||||
function handleHidden() {
|
||||
console.log("panel is being hidden");
|
||||
}
|
||||
|
||||
/**
|
||||
Create a panel, and add listeners for panel show/hide events.
|
||||
*/
|
||||
browser.devtools.panels.create(
|
||||
"My Panel",
|
||||
"icons/star.png",
|
||||
"devtools/panel/panel.html"
|
||||
).then((newPanel) => {
|
||||
newPanel.onShown.addListener(handleShown);
|
||||
newPanel.onHidden.addListener(handleHidden);
|
||||
});
|
||||
72
devtools-panels/devtools/panel/devtools-panel.js
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
Handle errors from the injected script.
|
||||
Errors may come from evaluating the JavaScript itself
|
||||
or from the devtools framework.
|
||||
See https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/devtools.inspectedWindow/eval#Return_value
|
||||
*/
|
||||
function handleError(error) {
|
||||
if (error.isError) {
|
||||
console.log(`Devtools error: ${error.code}`);
|
||||
} else {
|
||||
console.log(`JavaScript error: ${error.value}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Handle the result of evaluating the script.
|
||||
If there was an error, call handleError.
|
||||
*/
|
||||
function handleResult(result) {
|
||||
if (result[1]) {
|
||||
handleError(result[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Handle the result of evaluating the jQuery test script.
|
||||
Log the result of the test, or
|
||||
if there was an error, call handleError.
|
||||
*/
|
||||
function handlejQueryResult(result) {
|
||||
if (result[0] !== undefined) {
|
||||
console.log(`jQuery: ${result[0]}`);
|
||||
} else if (result[1]) {
|
||||
handleError(result[1]);
|
||||
}
|
||||
}
|
||||
/**
|
||||
When the user clicks the 'jquery' button,
|
||||
evaluate the jQuery script.
|
||||
*/
|
||||
const checkjQuery = "typeof jQuery != 'undefined'";
|
||||
document.getElementById("button_jquery").addEventListener("click", () => {
|
||||
browser.devtools.inspectedWindow.eval(checkjQuery)
|
||||
.then(handlejQueryResult);
|
||||
});
|
||||
/**
|
||||
When the user clicks each of the first three buttons,
|
||||
evaluate the corresponding script.
|
||||
*/
|
||||
const evalString = "$0.style.backgroundColor = 'red'";
|
||||
document.getElementById("button_background").addEventListener("click", () => {
|
||||
browser.devtools.inspectedWindow.eval(evalString)
|
||||
.then(handleResult);
|
||||
});
|
||||
|
||||
const inspectString = "inspect(document.querySelector('h1'))";
|
||||
document.getElementById("button_h1").addEventListener("click", () => {
|
||||
browser.devtools.inspectedWindow.eval(inspectString)
|
||||
.then(handleResult);
|
||||
});
|
||||
|
||||
/**
|
||||
When the user clicks the 'message' button,
|
||||
send a message to the background script.
|
||||
*/
|
||||
const scriptToAttach = "document.body.innerHTML = 'Hi from the devtools';";
|
||||
document.getElementById("button_message").addEventListener("click", () => {
|
||||
browser.runtime.sendMessage({
|
||||
tabId: browser.devtools.inspectedWindow.tabId,
|
||||
script: scriptToAttach
|
||||
});
|
||||
});
|
||||
14
devtools-panels/devtools/panel/panel.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
<button id="button_h1">Inspect h1</button>
|
||||
<button id="button_background">Reddinate inspected element</button>
|
||||
<button id="button_jquery">Check for jquery</button>
|
||||
<button id="button_message">Inject content script</button>
|
||||
<script src="devtools-panel.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
devtools-panels/icons/star.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
22
devtools-panels/manifest.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"description": "Adds a new panel to the developer tools. The panel contains buttons that demonstrate various basic features of the devtools API.",
|
||||
"manifest_version": 2,
|
||||
"name": "devtools-panels",
|
||||
"version": "1.0",
|
||||
"author": "Christophe Villeneuve",
|
||||
"homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/devtools-panels",
|
||||
"icons": {
|
||||
"48": "icons/star.png"
|
||||
},
|
||||
|
||||
"background": {
|
||||
"scripts": ["background_scripts/background.js"]
|
||||
},
|
||||
|
||||
"permissions": [
|
||||
"<all_urls>"
|
||||
],
|
||||
|
||||
"devtools_page": "devtools/devtools-page.html"
|
||||
|
||||
}
|
||||
8
embedded-webextension-bootstrapped/.eslintrc.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"amd": true,
|
||||
"webextensions": true
|
||||
}
|
||||
}
|
||||
2
embedded-webextension-overlay/chrome.manifest
Normal file
@@ -0,0 +1,2 @@
|
||||
content my-overlay-addon content/
|
||||
overlay chrome://browser/content/browser.xul chrome://my-overlay-addon/content/overlay.xul
|
||||
31
embedded-webextension-overlay/content/init.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/* globals Components, dump */
|
||||
|
||||
{
|
||||
const addonId = "my-overlay-addon@me";
|
||||
const {
|
||||
AddonManager,
|
||||
} = Components.utils.import("resource://gre/modules/AddonManager.jsm", {});
|
||||
|
||||
AddonManager.getAddonByID(addonId, addon => {
|
||||
const baseURI = addon.getResourceURI("/");
|
||||
|
||||
const {
|
||||
LegacyExtensionsUtils,
|
||||
} = Components.utils.import("resource://gre/modules/LegacyExtensionsUtils.jsm");
|
||||
|
||||
const myOverlayEmbeddedWebExtension = LegacyExtensionsUtils.getEmbeddedExtensionFor({
|
||||
id: addonId, resourceURI: baseURI,
|
||||
});
|
||||
|
||||
myOverlayEmbeddedWebExtension.startup().then(({browser}) => {
|
||||
dump(`${addonId} - embedded webext started\n`);
|
||||
browser.runtime.onMessage.addListener(msg => {
|
||||
dump(`${addonId} - received message from embedded webext ${msg}\n`);
|
||||
});
|
||||
}).catch(err => {
|
||||
Components.utils.reportError(
|
||||
`${addonId} - embedded webext startup failed: ${err.message} ${err.stack}\n`
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
4
embedded-webextension-overlay/content/overlay.xul
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0"?>
|
||||
<overlay id="myOverlayAddon" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
||||
<script src="chrome://my-overlay-addon/content/init.js"></script>
|
||||
</overlay>
|
||||
23
embedded-webextension-overlay/install.rdf
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0"?>
|
||||
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
|
||||
|
||||
<Description about="urn:mozilla:install-manifest">
|
||||
<em:id>my-overlay-addon@me</em:id>
|
||||
<em:version>1.0.1</em:version>
|
||||
<em:name>My Legacy Overlay Addon</em:name>
|
||||
|
||||
<em:type>2</em:type>
|
||||
<em:multiprocessCompatible>true</em:multiprocessCompatible>
|
||||
|
||||
<em:targetApplication>
|
||||
<Description>
|
||||
<!-- firefox -->
|
||||
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
|
||||
<em:minVersion>51.0</em:minVersion>
|
||||
<em:maxVersion>*</em:maxVersion>
|
||||
</Description>
|
||||
</em:targetApplication>
|
||||
|
||||
</Description>
|
||||
</RDF>
|
||||
3
embedded-webextension-overlay/webextension/background.js
Normal file
@@ -0,0 +1,3 @@
|
||||
console.log("Embedded WebExtension", window.location.href);
|
||||
|
||||
browser.runtime.sendMessage("embedded_webext -> overlay addon container");
|
||||
10
embedded-webextension-overlay/webextension/manifest.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Overlay Addon WebExtension",
|
||||
"version": "1.0.1",
|
||||
"description": "test embedding a webextension in a legacy overlay addon",
|
||||
|
||||
"background": {
|
||||
"scripts": ["background.js"]
|
||||
}
|
||||
}
|
||||
8
embedded-webextension-sdk/.eslintrc.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"amd": true,
|
||||
"webextensions": true
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
* This file contains the Map of word --> emoji substitutions.
|
||||
*/
|
||||
|
||||
/* exported sortedEmojiMap */
|
||||
|
||||
let dictionary = new Map();
|
||||
dictionary.set('apple', '🍎');
|
||||
dictionary.set('banana', '🍌');
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* all occurrences of each mapped word with its emoji counterpart.
|
||||
*/
|
||||
|
||||
/*global sortedEmojiMap*/
|
||||
|
||||
// emojiMap.js defines the 'sortedEmojiMap' variable.
|
||||
// Referenced here to reduce confusion.
|
||||
const emojiMap = sortedEmojiMap;
|
||||
|
||||
@@ -82,12 +82,12 @@
|
||||
"commands.onCommand"
|
||||
],
|
||||
"name": "commands",
|
||||
"description": "Demonstrates using the commands API to set up a keyboard shortcut. The shortcut created is accessed using Ctrl+Shift+Y (Command+Shift+Y on a Mac)."
|
||||
"description": "Demonstrates using the commands API to set up a keyboard shortcut. The shortcut created is accessed using Ctrl+Shift+U (Command+Shift+U on a Mac)."
|
||||
},
|
||||
{
|
||||
"javascript_apis": [
|
||||
"contextMenus.create",
|
||||
"contextMenus.onClicked",
|
||||
"menus.create",
|
||||
"menus.onClicked",
|
||||
"tabs.executeScript"
|
||||
],
|
||||
"name": "context-menu-copy-link-with-types",
|
||||
@@ -95,16 +95,16 @@
|
||||
},
|
||||
{
|
||||
"javascript_apis": [
|
||||
"contextMenus.create",
|
||||
"contextMenus.onClicked",
|
||||
"contextMenus.remove",
|
||||
"contextMenus.update",
|
||||
"menus.create",
|
||||
"menus.onClicked",
|
||||
"menus.remove",
|
||||
"menus.update",
|
||||
"i18n.getMessage",
|
||||
"runtime.lastError",
|
||||
"tabs.executeScript"
|
||||
],
|
||||
"name": "context-menu-demo",
|
||||
"description": "Demonstrates adding and manipulating context menu items using the contextMenus API."
|
||||
"name": "menu-demo",
|
||||
"description": "Demonstrates adding and manipulating menu items using the menus API."
|
||||
},
|
||||
{
|
||||
"javascript_apis": [
|
||||
@@ -170,7 +170,7 @@
|
||||
{
|
||||
"javascript_apis": [],
|
||||
"name": "eslint-example",
|
||||
"description": "Demonstrates how to configure a WebExtension with eslint."
|
||||
"description": "Demonstrates how to configure an extension with eslint."
|
||||
},
|
||||
{
|
||||
"javascript_apis": [
|
||||
@@ -223,6 +223,16 @@
|
||||
"name": "history-deleter",
|
||||
"description": "History API demo: deletes history items for a given domain"
|
||||
},
|
||||
{
|
||||
"javascript_apis": [
|
||||
"runtime.onMessage",
|
||||
"tabs.executeScript",
|
||||
"tabs.query",
|
||||
"tabs.sendMessage"
|
||||
],
|
||||
"name": "imagify",
|
||||
"description": "Using a sidebar, illustrates the use of file picker and drag and drop. A content script replaces the current page content with the chosen image."
|
||||
},
|
||||
{
|
||||
"javascript_apis": [
|
||||
"downloads.erase",
|
||||
@@ -247,7 +257,7 @@
|
||||
"runtime.sendMessage"
|
||||
],
|
||||
"name": "mocha-client-tests",
|
||||
"description": "This example shows two methods of testing a WebExtension: running tests from within the add-on, and running tests from the command line using Karma"
|
||||
"description": "This example shows two methods of testing an extension: running tests from within the extension, and running tests from the command line using Karma"
|
||||
},
|
||||
{
|
||||
"javascript_apis": [
|
||||
@@ -255,7 +265,7 @@
|
||||
"runtime.connectNative"
|
||||
],
|
||||
"name": "native-messaging",
|
||||
"description": "Example of native messaging, including a Python application and a WebExtension which exchanges messages with it."
|
||||
"description": "Example of native messaging, including a Python application and an extension which exchanges messages with it."
|
||||
},
|
||||
{
|
||||
"javascript_apis": [
|
||||
@@ -305,7 +315,7 @@
|
||||
"javascript_apis": [
|
||||
"extension.getURL",
|
||||
"proxy.onProxyError",
|
||||
"proxy.registerProxyScript",
|
||||
"proxy.register",
|
||||
"runtime.onMessage",
|
||||
"runtime.sendMessage",
|
||||
"storage.local",
|
||||
@@ -366,6 +376,12 @@
|
||||
"name": "theme-switcher",
|
||||
"description": "An example of how to use the management API for themes."
|
||||
},
|
||||
{
|
||||
"javascript_apis": [
|
||||
],
|
||||
"name": "themes",
|
||||
"description": "A collection of themes illustrating:<ul><li>weta_fade: a basic theme employing a single image specified in <code>headerURL:</code>.</li><li>weta_fade_chrome: the weta_fade theme implemented with Chrome compatible manifest keys.</li><li>weta_tiled: a theme using a tiled image.</li><li>weta_mirror: a theme using multiple images and aligning those images in the header.</li><li>animated: use of an animated PNG.</li></ul>"
|
||||
},
|
||||
{
|
||||
"javascript_apis": [
|
||||
"topSites.get"
|
||||
@@ -387,7 +403,7 @@
|
||||
"runtime.sendMessage"
|
||||
],
|
||||
"name": "webpack-modules",
|
||||
"description": "Demonstrates how to use webpack to package npm modules in a WebExtension."
|
||||
"description": "Demonstrates how to use webpack to package npm modules in an extension."
|
||||
},
|
||||
{
|
||||
"javascript_apis": [
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* exported getAccessToken */
|
||||
|
||||
const REDIRECT_URL = browser.identity.getRedirectURL();
|
||||
const CLIENT_ID = "YOUR-CLIENT-ID";
|
||||
const SCOPES = ["openid", "email", "profile"];
|
||||
@@ -10,7 +12,7 @@ const AUTH_URL =
|
||||
const VALIDATION_BASE_URL="https://www.googleapis.com/oauth2/v3/tokeninfo";
|
||||
|
||||
function extractAccessToken(redirectUri) {
|
||||
let m = redirectUri.match(/[#\?](.*)/);
|
||||
let m = redirectUri.match(/[#?](.*)/);
|
||||
if (!m || m.length < 1)
|
||||
return null;
|
||||
let params = new URLSearchParams(m[1].split("#")[0]);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/*global getAccessToken*/
|
||||
|
||||
function notifyUser(user) {
|
||||
browser.notifications.create({
|
||||
"type": "basic",
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
Fetch the user's info, passing in the access token in the Authorization
|
||||
HTTP request header.
|
||||
*/
|
||||
|
||||
/* exported getUserInfo */
|
||||
|
||||
function getUserInfo(accessToken) {
|
||||
const requestURL = "https://www.googleapis.com/oauth2/v1/userinfo?alt=json";
|
||||
const requestHeaders = new Headers();
|
||||
|
||||
@@ -7,7 +7,7 @@ function get_hostname(url) {
|
||||
}
|
||||
|
||||
function set_domain(domain) {
|
||||
spans = document.getElementsByClassName('domain');
|
||||
const spans = document.getElementsByClassName('domain');
|
||||
[].slice.call(spans).forEach((span) => {
|
||||
span.textContent = domain;
|
||||
});
|
||||
@@ -65,7 +65,7 @@ function clearAll(e) {
|
||||
// Loop through them and delete them one by one.
|
||||
var searchingHistory = browser.history.search({text: hostname})
|
||||
searchingHistory.then((results) => {
|
||||
for (k = 0; k < results.length; k++) {
|
||||
for (let k of results) {
|
||||
browser.history.deleteUrl({url: results[k].url});
|
||||
}
|
||||
// Clear out the UI.
|
||||
|
||||
26
imagify/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# imagify
|
||||
|
||||
**This add-on injects JavaScript into web pages. The `addons.mozilla.org` domain disallows this operation, so this add-on will not work properly when it's run on pages in the `addons.mozilla.org` domain.**
|
||||
|
||||
## What it does ##
|
||||
|
||||
The extension includes:
|
||||
|
||||
* a sidebar including HTML, CSS, and JavaScript
|
||||
* a content script
|
||||
* a web page template, packaged as web accessible resources
|
||||
|
||||
When the extension loads the user is offered a file picker and drop zone as methods to choose an image file.
|
||||
|
||||
Once an image file has been chosen, the extension injects the content script into the active tab, and sends the content script a message containing the URL of the chosen image.
|
||||
|
||||
When the content script receives this message, it replaces the current page content with the chosen image file.
|
||||
|
||||
## What it shows ##
|
||||
|
||||
How to:
|
||||
* write a sidebar
|
||||
* implement a file picker and drag and drop zone
|
||||
* give a sidebar style and behavior using CSS and JavaScript
|
||||
* inject a content script programmatically using `tabs.executeScript()`
|
||||
* send a message from the main extension to a content script
|
||||
47
imagify/content_scripts/content.js
Normal file
@@ -0,0 +1,47 @@
|
||||
(function() {
|
||||
/*
|
||||
Check and set a global guard variable.
|
||||
If this content script is injected into the same page again,
|
||||
it will do nothing next time.
|
||||
*/
|
||||
if (window.hasRun) {
|
||||
return;
|
||||
}
|
||||
window.hasRun = true;
|
||||
|
||||
/*
|
||||
Add the image to the web page by:
|
||||
* Removing every node in the document.body
|
||||
* Inserting the selected image
|
||||
*/
|
||||
function injectImage(request, sender, sendResponse) {
|
||||
removeEverything();
|
||||
insertImage(request.imageURL);
|
||||
}
|
||||
|
||||
/*
|
||||
Remove every node under document.body
|
||||
*/
|
||||
function removeEverything() {
|
||||
while (document.body.firstChild) {
|
||||
document.body.firstChild.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Given a URL to an image, create and style an iframe containing an
|
||||
IMG node pointing to that image, then insert the node into the document.
|
||||
*/
|
||||
function insertImage(imageURL) {
|
||||
const insertImage = document.createElement("iframe");
|
||||
insertImage.setAttribute("src", browser.extension.getURL(`/viewer.html?blobURL=${imageURL}`));
|
||||
insertImage.setAttribute("style", "width: 100vw; height: 100vh;");
|
||||
document.body.appendChild(insertImage);
|
||||
}
|
||||
|
||||
/*
|
||||
Assign injectImage() as a listener for messages from the extension.
|
||||
*/
|
||||
browser.runtime.onMessage.addListener(injectImage);
|
||||
|
||||
})();
|
||||
22
imagify/manifest.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
|
||||
"description": "Adds a sidebar offerin a file picker and drap and drop zone. When an image file is chosen the active tab's body content is replaced with file selected. See https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Examples#imagify",
|
||||
"manifest_version": 2,
|
||||
"name": "Imagify",
|
||||
"version": "1.0",
|
||||
"homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/imagify",
|
||||
|
||||
"permissions": [
|
||||
"tabs",
|
||||
"<all_urls>"
|
||||
],
|
||||
|
||||
"sidebar_action": {
|
||||
"default_title": "Imagify",
|
||||
"default_panel": "sidebar/sidebar.html"
|
||||
},
|
||||
|
||||
"web_accessible_resources": [
|
||||
"/viewer.html"
|
||||
]
|
||||
}
|
||||
63
imagify/sidebar/choose_file.js
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
Listens for a file being selected, creates a ObjectURL for the chosen file, injects a
|
||||
content script into the active tab then passes the image URL through a message to the
|
||||
active tab ID.
|
||||
*/
|
||||
|
||||
// Listen for a file being selected through the file picker
|
||||
const inputElement = document.getElementById("input");
|
||||
inputElement.addEventListener("change", handlePicked, false);
|
||||
|
||||
// Listen for a file being dropped into the drop zone
|
||||
const dropbox = document.getElementById("drop_zone");
|
||||
dropbox.addEventListener("dragenter", dragenter, false);
|
||||
dropbox.addEventListener("dragover", dragover, false);
|
||||
dropbox.addEventListener("drop", drop, false);
|
||||
|
||||
// Get the image file if it was chosen from the pick list
|
||||
function handlePicked() {
|
||||
displayFile(this.files);
|
||||
}
|
||||
|
||||
// Get the image file if it was dragged into the sidebar drop zone
|
||||
function drop(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
displayFile(e.dataTransfer.files);
|
||||
}
|
||||
|
||||
/*
|
||||
Insert the content script and send the image file ObjectURL to the content script using a
|
||||
message.
|
||||
*/
|
||||
function displayFile(fileList) {
|
||||
const imageURL = window.URL.createObjectURL(fileList[0]);
|
||||
|
||||
browser.tabs.executeScript({
|
||||
file: "/content_scripts/content.js"
|
||||
}).then(messageContent)
|
||||
.catch(reportError);
|
||||
|
||||
function messageContent() {
|
||||
const gettingActiveTab = browser.tabs.query({active: true, currentWindow: true});
|
||||
gettingActiveTab.then((tabs) => {
|
||||
browser.tabs.sendMessage(tabs[0].id, {imageURL});
|
||||
});
|
||||
}
|
||||
|
||||
function reportError(error) {
|
||||
console.error(`Could not inject content script: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore the drag enter event - not used in this extension
|
||||
function dragenter(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Ignore the drag over event - not used in this extension
|
||||
function dragover(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
15
imagify/sidebar/sidebar.css
Normal file
@@ -0,0 +1,15 @@
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#drop_zone {
|
||||
border: 5px solid blue;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#drop_zone_label {
|
||||
margin: 1em;
|
||||
display: block;
|
||||
}
|
||||
24
imagify/sidebar/sidebar.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="sidebar.css"/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="picker_zone" style="margin:20px ;color:blue">
|
||||
<input type="file" accept="image/*" id="input">
|
||||
</div>
|
||||
|
||||
<div id="drop_zone">
|
||||
<strong id="drop_zone_label">Drag an image file into this Drop Zone ...</strong>
|
||||
</div>
|
||||
|
||||
<script src="choose_file.js"></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
8
imagify/viewer.css
Normal file
@@ -0,0 +1,8 @@
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
17
imagify/viewer.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE>
|
||||
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="viewer.css"/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<img src="about:blank">
|
||||
<script src="viewer.js"></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
3
imagify/viewer.js
Normal file
@@ -0,0 +1,3 @@
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const imageBlobURL = params.get("blobURL");
|
||||
document.querySelector("img").setAttribute("src", imageBlobURL);
|
||||
@@ -1,6 +1,6 @@
|
||||
function showCookiesForTab(tabs) {
|
||||
//get the first tab object in the array
|
||||
tab = tabs.pop();
|
||||
let tab = tabs.pop();
|
||||
|
||||
//get all cookies in the domain
|
||||
var gettingAllCookies = browser.cookies.getAll({url: tab.url});
|
||||
@@ -14,22 +14,22 @@ function showCookiesForTab(tabs) {
|
||||
|
||||
if (cookies.length > 0) {
|
||||
//add an <li> item with the name and value of the cookie to the list
|
||||
for (cookie of cookies) {
|
||||
var li = document.createElement("li");
|
||||
var content = document.createTextNode(cookie.name + ": "+ cookie.value);
|
||||
for (let cookie of cookies) {
|
||||
let li = document.createElement("li");
|
||||
let content = document.createTextNode(cookie.name + ": "+ cookie.value);
|
||||
li.appendChild(content);
|
||||
cookieList.appendChild(li);
|
||||
}
|
||||
} else {
|
||||
var p = document.createElement("p");
|
||||
var content = document.createTextNode("No cookies in this tab.");
|
||||
var parent = cookieList.parentNode;
|
||||
let p = document.createElement("p");
|
||||
let content = document.createTextNode("No cookies in this tab.");
|
||||
let parent = cookieList.parentNode;
|
||||
|
||||
p.appendChild(content);
|
||||
parent.appendChild(p);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
//get active tab to run an callback function.
|
||||
//it sends to our callback an array of tab objects
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# context-menu-demo
|
||||
# menu-demo
|
||||
|
||||
A demo of the [contextMenus API](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/contextMenus/).
|
||||
A demo of the [menus API](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/menus/).
|
||||
|
||||
**This add-on injects JavaScript into web pages. The `addons.mozilla.org` domain disallows this operation, so this add-on will not work properly when it's run on pages in the `addons.mozilla.org` domain.**
|
||||
|
||||
**This add-on uses the `menus` namespace to access the functions it needs to create menu items. Note that Chrome, Edge, and Opera all use the `contextMenus` namespace for this, so this extension will not work in these browsers. For compatibility with these browsers, Firefox also offers the `contextMenus` namespace, so to make this extension work with other browsers, use `contextMenus`.**
|
||||
|
||||
## What it does
|
||||
|
||||
This add-on adds several items to the browser's context menu:
|
||||
@@ -20,9 +22,11 @@ like about:debugging.
|
||||
item is clicked.
|
||||
* one item that uses the "commands" property to open the add-on's sidebar.
|
||||
|
||||
It also adds one item to the browser's "Tools" menu.
|
||||
|
||||
## What it shows
|
||||
|
||||
* How to create various types of context menu item:
|
||||
* How to create various types of menu item:
|
||||
* normal
|
||||
* radio
|
||||
* separator
|
||||
@@ -1,47 +1,52 @@
|
||||
{
|
||||
"extensionName": {
|
||||
"message": "Context menu demo",
|
||||
"message": "Menu demo",
|
||||
"description": "Name of the extension."
|
||||
},
|
||||
|
||||
"extensionDescription": {
|
||||
"message": "Demonstrates the contextMenus API.",
|
||||
"message": "Demonstrates the menus API.",
|
||||
"description": "Description of the add-on."
|
||||
},
|
||||
|
||||
"contextMenuItemSelectionLogger": {
|
||||
"menuItemSelectionLogger": {
|
||||
"message": "Log '%s' to the browser console",
|
||||
"description": "Title of context menu item that logs the selected text when clicked."
|
||||
},
|
||||
|
||||
"contextMenuItemRemoveMe": {
|
||||
"menuItemRemoveMe": {
|
||||
"message": "Remove me!",
|
||||
"description": "Title of context menu item that removes itself when clicked."
|
||||
},
|
||||
|
||||
"contextMenuItemGreenify": {
|
||||
"menuItemGreenify": {
|
||||
"message": "Greenify",
|
||||
"description": "Title of context menu item that adds a green border when clicked."
|
||||
},
|
||||
|
||||
"contextMenuItemBluify": {
|
||||
"menuItemBluify": {
|
||||
"message": "Bluify",
|
||||
"description": "Title of context menu item that adds a green border when clicked."
|
||||
},
|
||||
|
||||
"contextMenuItemCheckMe": {
|
||||
"menuItemCheckMe": {
|
||||
"message": "Check me",
|
||||
"description": "Title of context menu item when the item is checked."
|
||||
},
|
||||
|
||||
"contextMenuItemUncheckMe": {
|
||||
"menuItemUncheckMe": {
|
||||
"message": "Uncheck me",
|
||||
"description": "Title of context menu item when the item is unchecked."
|
||||
},
|
||||
|
||||
"contextMenuItemOpenSidebar": {
|
||||
"menuItemOpenSidebar": {
|
||||
"message": "Open sidebar",
|
||||
"description": "Title of context menu item that opens a sidebar."
|
||||
},
|
||||
|
||||
"menuItemToolsMenu": {
|
||||
"message": "Click me!",
|
||||
"description": "Title of tools menu item."
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
Called when the item has been created, or when creation failed due to an error.
|
||||
We'll just log success/failure here.
|
||||
*/
|
||||
function onCreated(n) {
|
||||
function onCreated() {
|
||||
if (browser.runtime.lastError) {
|
||||
console.log(`Error: ${browser.runtime.lastError}`);
|
||||
} else {
|
||||
@@ -29,41 +29,41 @@ function onError(error) {
|
||||
/*
|
||||
Create all the context menu items.
|
||||
*/
|
||||
browser.contextMenus.create({
|
||||
browser.menus.create({
|
||||
id: "log-selection",
|
||||
title: browser.i18n.getMessage("contextMenuItemSelectionLogger"),
|
||||
title: browser.i18n.getMessage("menuItemSelectionLogger"),
|
||||
contexts: ["selection"]
|
||||
}, onCreated);
|
||||
|
||||
browser.contextMenus.create({
|
||||
browser.menus.create({
|
||||
id: "remove-me",
|
||||
title: browser.i18n.getMessage("contextMenuItemRemoveMe"),
|
||||
title: browser.i18n.getMessage("menuItemRemoveMe"),
|
||||
contexts: ["all"]
|
||||
}, onCreated);
|
||||
|
||||
browser.contextMenus.create({
|
||||
browser.menus.create({
|
||||
id: "separator-1",
|
||||
type: "separator",
|
||||
contexts: ["all"]
|
||||
}, onCreated);
|
||||
|
||||
browser.contextMenus.create({
|
||||
browser.menus.create({
|
||||
id: "greenify",
|
||||
type: "radio",
|
||||
title: browser.i18n.getMessage("contextMenuItemGreenify"),
|
||||
title: browser.i18n.getMessage("menuItemGreenify"),
|
||||
contexts: ["all"],
|
||||
checked: true
|
||||
}, onCreated);
|
||||
|
||||
browser.contextMenus.create({
|
||||
browser.menus.create({
|
||||
id: "bluify",
|
||||
type: "radio",
|
||||
title: browser.i18n.getMessage("contextMenuItemBluify"),
|
||||
title: browser.i18n.getMessage("menuItemBluify"),
|
||||
contexts: ["all"],
|
||||
checked: false
|
||||
}, onCreated);
|
||||
|
||||
browser.contextMenus.create({
|
||||
browser.menus.create({
|
||||
id: "separator-2",
|
||||
type: "separator",
|
||||
contexts: ["all"]
|
||||
@@ -71,22 +71,27 @@ browser.contextMenus.create({
|
||||
|
||||
var checkedState = true;
|
||||
|
||||
browser.contextMenus.create({
|
||||
browser.menus.create({
|
||||
id: "check-uncheck",
|
||||
type: "checkbox",
|
||||
title: browser.i18n.getMessage("contextMenuItemUncheckMe"),
|
||||
title: browser.i18n.getMessage("menuItemUncheckMe"),
|
||||
contexts: ["all"],
|
||||
checked: checkedState
|
||||
}, onCreated);
|
||||
|
||||
|
||||
browser.contextMenus.create({
|
||||
browser.menus.create({
|
||||
id: "open-sidebar",
|
||||
title: browser.i18n.getMessage("contextMenuItemOpenSidebar"),
|
||||
title: browser.i18n.getMessage("menuItemOpenSidebar"),
|
||||
contexts: ["all"],
|
||||
command: "_execute_sidebar_action"
|
||||
}, onCreated);
|
||||
|
||||
browser.menus.create({
|
||||
id: "tools-menu",
|
||||
title: browser.i18n.getMessage("menuItemToolsMenu"),
|
||||
contexts: ["tools_menu"],
|
||||
}, onCreated);
|
||||
|
||||
/*
|
||||
Set a colored border on the document in the given tab.
|
||||
|
||||
@@ -113,12 +118,12 @@ property into the event listener.
|
||||
function updateCheckUncheck() {
|
||||
checkedState = !checkedState;
|
||||
if (checkedState) {
|
||||
browser.contextMenus.update("check-uncheck", {
|
||||
title: browser.i18n.getMessage("contextMenuItemUncheckMe"),
|
||||
browser.menus.update("check-uncheck", {
|
||||
title: browser.i18n.getMessage("menuItemUncheckMe"),
|
||||
});
|
||||
} else {
|
||||
browser.contextMenus.update("check-uncheck", {
|
||||
title: browser.i18n.getMessage("contextMenuItemCheckMe"),
|
||||
browser.menus.update("check-uncheck", {
|
||||
title: browser.i18n.getMessage("menuItemCheckMe"),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -127,13 +132,13 @@ function updateCheckUncheck() {
|
||||
The click event listener, where we perform the appropriate action given the
|
||||
ID of the menu item that was clicked.
|
||||
*/
|
||||
browser.contextMenus.onClicked.addListener(function(info, tab) {
|
||||
browser.menus.onClicked.addListener((info, tab) => {
|
||||
switch (info.menuItemId) {
|
||||
case "log-selection":
|
||||
console.log(info.selectionText);
|
||||
break;
|
||||
case "remove-me":
|
||||
var removing = browser.contextMenus.remove(info.menuItemId);
|
||||
var removing = browser.menus.remove(info.menuItemId);
|
||||
removing.then(onRemoved, onError);
|
||||
break;
|
||||
case "bluify":
|
||||
@@ -148,5 +153,8 @@ browser.contextMenus.onClicked.addListener(function(info, tab) {
|
||||
case "open-sidebar":
|
||||
console.log("Opening my sidebar");
|
||||
break;
|
||||
case "tools-menu":
|
||||
console.log("Clicked the tools menu item");
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
Before Width: | Height: | Size: 251 B After Width: | Height: | Size: 251 B |
|
Before Width: | Height: | Size: 344 B After Width: | Height: | Size: 344 B |
|
Before Width: | Height: | Size: 310 B After Width: | Height: | Size: 310 B |
@@ -7,7 +7,7 @@
|
||||
"default_locale": "en",
|
||||
"applications": {
|
||||
"gecko": {
|
||||
"strict_min_version": "55.0a1"
|
||||
"strict_min_version": "56.0a1"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
},
|
||||
|
||||
"permissions": [
|
||||
"contextMenus",
|
||||
"menus",
|
||||
"activeTab"
|
||||
],
|
||||
|
||||
8
mocha-client-tests/.eslintrc.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"amd": true,
|
||||
"webextensions": true
|
||||
}
|
||||
}
|
||||
@@ -12,4 +12,4 @@ var Background = {
|
||||
}
|
||||
};
|
||||
|
||||
chrome.runtime.onMessage.addListener(Background.receiveMessage);
|
||||
browser.runtime.onMessage.addListener(Background.receiveMessage);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
if($game.innerText !== 'ping'){
|
||||
$game.innerText = 'ping';
|
||||
} else{
|
||||
chrome.runtime.sendMessage({action: 'ping'},function(response) {
|
||||
browser.runtime.sendMessage({action: 'ping'}).then((response) => {
|
||||
$game.innerText = response;
|
||||
});
|
||||
}
|
||||
|
||||
32
package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "webextensions-examples",
|
||||
"title": "WebExtensions Examples",
|
||||
"version": "1.0.0",
|
||||
"description": "Example Firefox add-ons created using the WebExtensions API",
|
||||
"devDependencies": {
|
||||
"eslint": "^4.4.1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/mdn/webextensions-examples.git"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "eslint .",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/mdn/webextensions-examples/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"webextensions",
|
||||
"webextensions-apis",
|
||||
"mdn",
|
||||
"firefox",
|
||||
"mozilla"
|
||||
],
|
||||
"homepage": "https://developer.mozilla.org/Add-ons/WebExtensions/Examples",
|
||||
"dependencies": {
|
||||
"babel-eslint": "^7.2.3"
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
Listen for messages from the page.
|
||||
If the message was from the page script, show an alert.
|
||||
*/
|
||||
window.addEventListener("message", function(event) {
|
||||
window.addEventListener("message", (event) => {
|
||||
if (event.source == window &&
|
||||
event.data &&
|
||||
event.data.direction == "from-page-script") {
|
||||
|
||||
5
permissions/.eslintrc.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 8
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ const defaultSettings = {
|
||||
}
|
||||
|
||||
// Register the proxy script
|
||||
browser.proxy.registerProxyScript(proxyScriptURL);
|
||||
browser.proxy.register(proxyScriptURL);
|
||||
|
||||
// Log any errors from the proxy script
|
||||
browser.proxy.onProxyError.addListener(error => {
|
||||
@@ -15,7 +15,7 @@ browser.proxy.onProxyError.addListener(error => {
|
||||
});
|
||||
|
||||
// Initialize the proxy
|
||||
function handleInit(message) {
|
||||
function handleInit() {
|
||||
// update the proxy whenever stored settings change
|
||||
browser.storage.onChanged.addListener((newSettings) => {
|
||||
browser.runtime.sendMessage(newSettings.blockedHosts.newValue, {toProxyScript: true});
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
"applications": {
|
||||
"gecko": {
|
||||
"strict_min_version": "55.0a1"
|
||||
"strict_min_version": "56.0a1"
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/* exported FindProxyForURL */
|
||||
|
||||
var blockedHosts = [];
|
||||
const allow = "DIRECT 1234";
|
||||
const allow = "DIRECT";
|
||||
const deny = "PROXY 127.0.0.1:65535";
|
||||
|
||||
// tell the background script that we are ready
|
||||
|
||||
@@ -27,7 +27,7 @@ function initialize() {
|
||||
var gettingAllStorageItems = browser.storage.local.get(null);
|
||||
gettingAllStorageItems.then((results) => {
|
||||
var noteKeys = Object.keys(results);
|
||||
for(noteKey of noteKeys) {
|
||||
for (let noteKey of noteKeys) {
|
||||
var curValue = results[noteKey];
|
||||
displayNote(noteKey,curValue);
|
||||
}
|
||||
@@ -88,8 +88,8 @@ function displayNote(title, body) {
|
||||
|
||||
/* set up listener for the delete functionality */
|
||||
|
||||
deleteBtn.addEventListener('click',function(e){
|
||||
evtTgt = e.target;
|
||||
deleteBtn.addEventListener('click',(e) => {
|
||||
const evtTgt = e.target;
|
||||
evtTgt.parentNode.parentNode.parentNode.removeChild(evtTgt.parentNode.parentNode);
|
||||
browser.storage.local.remove(title);
|
||||
})
|
||||
@@ -125,24 +125,24 @@ function displayNote(title, body) {
|
||||
|
||||
/* set up listeners for the update functionality */
|
||||
|
||||
noteH.addEventListener('click',function(){
|
||||
noteH.addEventListener('click',() => {
|
||||
noteDisplay.style.display = 'none';
|
||||
noteEdit.style.display = 'block';
|
||||
})
|
||||
|
||||
notePara.addEventListener('click',function(){
|
||||
notePara.addEventListener('click',() => {
|
||||
noteDisplay.style.display = 'none';
|
||||
noteEdit.style.display = 'block';
|
||||
})
|
||||
|
||||
cancelBtn.addEventListener('click',function(){
|
||||
cancelBtn.addEventListener('click',() => {
|
||||
noteDisplay.style.display = 'block';
|
||||
noteEdit.style.display = 'none';
|
||||
noteTitleEdit.value = title;
|
||||
noteBodyEdit.value = body;
|
||||
})
|
||||
|
||||
updateBtn.addEventListener('click',function(){
|
||||
updateBtn.addEventListener('click',() => {
|
||||
if(noteTitleEdit.value !== title || noteBodyEdit.value !== body) {
|
||||
updateNote(title,noteTitleEdit.value,noteBodyEdit.value);
|
||||
note.parentNode.removeChild(note);
|
||||
|
||||
15
react-es6-popup/.eslintrc.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"amd": true,
|
||||
"webextensions": true
|
||||
}
|
||||
}
|
||||
2
react-es6-popup/src/popup.js
vendored
@@ -11,7 +11,7 @@ class Popup extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
// Get the active tab and store it in component state.
|
||||
chrome.tabs.query({active: true}, tabs => {
|
||||
browser.tabs.query({active: true}).then(tabs => {
|
||||
this.setState({activeTab: tabs[0]});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
copy the selected text to clipboard
|
||||
*/
|
||||
function copySelection(e) {
|
||||
function copySelection() {
|
||||
var selectedText = window.getSelection().toString().trim();
|
||||
|
||||
if (selectedText) {
|
||||
|
||||
36
store-collected-images/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# "Image Reference Collector" example
|
||||
|
||||
## What it does
|
||||
|
||||
This example adds a context menu which targets any image element in the webpage.
|
||||
When the context menu item is clicked, the add-on opens a window and
|
||||
adds the related image element to the preview list of the collected images.
|
||||
The user can then store the collected images by giving the collection a name
|
||||
and pressing the **save** button.
|
||||
|
||||
Once a collection of reference images has been stored by the add-on, they
|
||||
can be navigated using the extension page that the add-on will open in a tab
|
||||
when the user press the add-on **browserAction**.
|
||||
|
||||
## What it shows
|
||||
|
||||
The main goal of this example is showing how to use the [idb-file-storage library](https://www.npmjs.com/package/idb-file-storage) to store and manipulate files in a WebExtension.
|
||||
|
||||
* How to store blob into the add-on IndexedDB storage
|
||||
* How to list the stored blobs (optionally by filtering the listed blobs)
|
||||
* How to turn the stored blobs into blob urls to show them in the extension page
|
||||
* How to remove the stored blobs from the extension IndexedDB storage.
|
||||
|
||||
[](https://youtu.be/t6aVqMMe2Rc)
|
||||
|
||||
This example is written in two forms:
|
||||
|
||||
- a plain webextension (which doesn't need any build step)
|
||||
- a webextension built using webpack
|
||||
|
||||
The code that stores and retrieves the files from the IndexedDB storage is in the
|
||||
file named `utils/image-store.js` in both the example version.
|
||||
|
||||
## Icons
|
||||
|
||||
The icon for this add-on is provided by [icons8](https://icons8.com/).
|
||||
BIN
store-collected-images/screenshots/screenshot.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
1
store-collected-images/webextension-plain/.eslintignore
Normal file
@@ -0,0 +1 @@
|
||||
deps
|
||||
3
store-collected-images/webextension-plain/.eslintrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
39
store-collected-images/webextension-plain/README.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# "Image Reference Collector" example without a webpack build step (and React UI)
|
||||
|
||||
## Usage
|
||||
|
||||
This version of the example doesn't use Webpack and Babel to transpile the ES6 modules (and JSX)
|
||||
into JavaScript bundle scripts, so it can be executed (using `web-ext run` or by installing it temporarily from "about:debugging#addons") and changed without any build step.
|
||||
|
||||
## NOTE on the plain JavaScript React UI
|
||||
|
||||
The UI of this example is based on React (as is the "build with webpack" version of this example), but it uses plain JavaScript instead of JSX (the "HTML"-like syntax usually used in "React"-based projects), and so the component UI hierarchy is composed of `React.createElement` function calls, e.g.
|
||||
|
||||
```
|
||||
class MyReactComponent extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="important">
|
||||
<h3>A title</h3>
|
||||
<p>A text paragraph</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
in plain Javascript (without JSX) this becomes:
|
||||
|
||||
```
|
||||
// Shortcut for React components render methods.
|
||||
const el = React.createElement;
|
||||
|
||||
class Popup extends React.Component {
|
||||
render() {
|
||||
return el("div", {className: "important"}, [
|
||||
el("h3", {}, "A title"),
|
||||
el("p", {}, "A text paragraph"),
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
47
store-collected-images/webextension-plain/background.js
Normal file
@@ -0,0 +1,47 @@
|
||||
// Open the UI to navigate the collection images in a tab.
|
||||
browser.browserAction.onClicked.addListener(() => {
|
||||
browser.tabs.create({url: "/navigate-collection.html"});
|
||||
});
|
||||
|
||||
// Add a context menu action on every image element in the page.
|
||||
browser.contextMenus.create({
|
||||
id: "collect-image",
|
||||
title: "Add to the collected images",
|
||||
contexts: ["image"],
|
||||
});
|
||||
|
||||
// Manage pending collected images.
|
||||
let pendingCollectedUrls = [];
|
||||
browser.runtime.onMessage.addListener((msg) => {
|
||||
if (msg.type === "get-pending-collected-urls") {
|
||||
let urls = pendingCollectedUrls;
|
||||
pendingCollectedUrls = [];
|
||||
return Promise.resolve(urls);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle the context menu action click events.
|
||||
browser.contextMenus.onClicked.addListener(async (info) => {
|
||||
try {
|
||||
await browser.runtime.sendMessage({
|
||||
type: "new-collected-images",
|
||||
url: info.srcUrl,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.message.includes("Could not establish connection. Receiving end does not exist.")) {
|
||||
// Add the url to the pending urls and open a popup.
|
||||
pendingCollectedUrls.push(info.srcUrl);
|
||||
try {
|
||||
await browser.windows.create({
|
||||
type: "popup", url: "/popup.html",
|
||||
top: 0, left: 0, width: 300, height: 400,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,801 @@
|
||||
(function (global, factory) {
|
||||
if (typeof define === "function" && define.amd) {
|
||||
define("idb-file-storage", ["exports"], factory);
|
||||
} else if (typeof exports !== "undefined") {
|
||||
factory(exports);
|
||||
} else {
|
||||
var mod = {
|
||||
exports: {}
|
||||
};
|
||||
factory(mod.exports);
|
||||
global.IDBFiles = mod.exports;
|
||||
}
|
||||
})(this, function (exports) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* @typedef {Object} IDBPromisedFileHandle.Metadata
|
||||
* @property {number} size
|
||||
* The size of the file in bytes.
|
||||
* @property {Date} last Modified
|
||||
* The time and date of the last change to the file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} IDBFileStorage.ListFilteringOptions
|
||||
* @property {string} startsWith
|
||||
* A string to be checked with `fileNameString.startsWith(...)`.
|
||||
* @property {string} endsWith
|
||||
* A string to be checked with `fileNameString.endsWith(...)`.
|
||||
* @property {string} includes
|
||||
* A string to be checked with `fileNameString.includes(...)`.
|
||||
* @property {function} filterFn
|
||||
* A function to be used to check the file name (`filterFn(fileNameString)`).
|
||||
*/
|
||||
|
||||
/**
|
||||
* Wraps a DOMRequest into a promise, optionally transforming the result using the onsuccess
|
||||
* callback.
|
||||
*
|
||||
* @param {IDBRequest|DOMRequest} req
|
||||
* The DOMRequest instance to wrap in a Promise.
|
||||
* @param {function} [onsuccess]
|
||||
* An optional onsuccess callback which can transform the result before resolving it.
|
||||
*
|
||||
* @returns {Promise}
|
||||
* The promise which wraps the request result, rejected if the request.onerror has been
|
||||
* called.
|
||||
*/
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.waitForDOMRequest = waitForDOMRequest;
|
||||
exports.getFileStorage = getFileStorage;
|
||||
function waitForDOMRequest(req, onsuccess) {
|
||||
return new Promise((resolve, reject) => {
|
||||
req.onsuccess = onsuccess ? () => resolve(onsuccess(req.result)) : () => resolve(req.result);
|
||||
req.onerror = () => reject(req.error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps an IDBMutableFile's FileHandle with a nicer Promise-based API.
|
||||
*
|
||||
* Instances of this class are created from the
|
||||
* {@link IDBPromisedMutableFile.open} method.
|
||||
*/
|
||||
class IDBPromisedFileHandle {
|
||||
/**
|
||||
* @private private helper method used internally.
|
||||
*/
|
||||
constructor({ file, lockedFile }) {
|
||||
// All the following properties are private and it should not be needed
|
||||
// while using the API.
|
||||
|
||||
/** @private */
|
||||
this.file = file;
|
||||
/** @private */
|
||||
this.lockedFile = lockedFile;
|
||||
/** @private */
|
||||
this.writeQueue = Promise.resolve();
|
||||
/** @private */
|
||||
this.closed = undefined;
|
||||
/** @private */
|
||||
this.aborted = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private private helper method used internally.
|
||||
*/
|
||||
ensureLocked({ invalidMode } = {}) {
|
||||
if (this.closed) {
|
||||
throw new Error("FileHandle has been closed");
|
||||
}
|
||||
|
||||
if (this.aborted) {
|
||||
throw new Error("FileHandle has been aborted");
|
||||
}
|
||||
|
||||
if (!this.lockedFile) {
|
||||
throw new Error("Invalid FileHandled");
|
||||
}
|
||||
|
||||
if (invalidMode && this.lockedFile.mode === invalidMode) {
|
||||
throw new Error(`FileHandle should not be opened as '${this.lockedFile.mode}'`);
|
||||
}
|
||||
if (!this.lockedFile.active) {
|
||||
// Automatically relock the file with the last open mode
|
||||
this.file.reopenFileHandle(this);
|
||||
}
|
||||
}
|
||||
|
||||
// Promise-based MutableFile API
|
||||
|
||||
/**
|
||||
* Provide access to the mode that has been used to open the {@link IDBPromisedMutableFile}.
|
||||
*
|
||||
* @type {"readonly"|"readwrite"|"writeonly"}
|
||||
*/
|
||||
get mode() {
|
||||
return this.lockedFile.mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* A boolean property that is true if the lock is still active.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
get active() {
|
||||
return this.lockedFile ? this.lockedFile.active : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the locked file (and wait for any written data to be flushed if needed).
|
||||
*
|
||||
* @returns {Promise}
|
||||
* A promise which is resolved when the close request has been completed
|
||||
*/
|
||||
async close() {
|
||||
if (!this.lockedFile) {
|
||||
throw new Error("FileHandle is not open");
|
||||
}
|
||||
|
||||
// Wait the queued write to complete.
|
||||
await this.writeQueue;
|
||||
|
||||
// Wait for flush request to complete if needed.
|
||||
if (this.lockedFile.active && this.lockedFile.mode !== "readonly") {
|
||||
await waitForDOMRequest(this.lockedFile.flush());
|
||||
}
|
||||
|
||||
this.closed = true;
|
||||
this.lockedFile = null;
|
||||
this.writeQueue = Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort any pending data request and set the instance as aborted.
|
||||
*
|
||||
* @returns {Promise}
|
||||
* A promise which is resolved when the abort request has been completed
|
||||
*/
|
||||
async abort() {
|
||||
if (this.lockedFile.active) {
|
||||
// NOTE: in the docs abort is reported to return a DOMRequest, but it doesn't seem
|
||||
// to be the case. (https://developer.mozilla.org/en-US/docs/Web/API/LockedFile/abort)
|
||||
this.lockedFile.abort();
|
||||
}
|
||||
|
||||
this.aborted = true;
|
||||
this.lockedFile = null;
|
||||
this.writeQueue = Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file metadata (take a look to {@link IDBPromisedFileHandle.Metadata} for more info).
|
||||
*
|
||||
* @returns {Promise<{size: number, lastModified: Date}>}
|
||||
* A promise which is resolved when the request has been completed
|
||||
*/
|
||||
async getMetadata() {
|
||||
this.ensureLocked();
|
||||
return waitForDOMRequest(this.lockedFile.getMetadata());
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a given amount of data from the file as Text (optionally starting from the specified
|
||||
* location).
|
||||
*
|
||||
* @param {number} size
|
||||
* The amount of data to read.
|
||||
* @param {number} [location]
|
||||
* The location where the request should start to read the data.
|
||||
*
|
||||
* @returns {Promise<string>}
|
||||
* A promise which resolves to the data read, when the request has been completed.
|
||||
*/
|
||||
async readAsText(size, location) {
|
||||
this.ensureLocked({ invalidMode: "writeonly" });
|
||||
if (typeof location === "number") {
|
||||
this.lockedFile.location = location;
|
||||
}
|
||||
return waitForDOMRequest(this.lockedFile.readAsText(size));
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a given amount of data from the file as an ArrayBufer (optionally starting from the specified
|
||||
* location).
|
||||
*
|
||||
* @param {number} size
|
||||
* The amount of data to read.
|
||||
* @param {number} [location]
|
||||
* The location where the request should start to read the data.
|
||||
*
|
||||
* @returns {Promise<ArrayBuffer>}
|
||||
* A promise which resolves to the data read, when the request has been completed.
|
||||
*/
|
||||
async readAsArrayBuffer(size, location) {
|
||||
this.ensureLocked({ invalidMode: "writeonly" });
|
||||
if (typeof location === "number") {
|
||||
this.lockedFile.location = location;
|
||||
}
|
||||
return waitForDOMRequest(this.lockedFile.readAsArrayBuffer(size));
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate the file (optionally at a specified location).
|
||||
*
|
||||
* @param {number} [location=0]
|
||||
* The location where the file should be truncated.
|
||||
*
|
||||
* @returns {Promise<ArrayBuffer>}
|
||||
* A promise which is resolved once the request has been completed.
|
||||
*/
|
||||
async truncate(location = 0) {
|
||||
this.ensureLocked({ invalidMode: "readonly" });
|
||||
return waitForDOMRequest(this.lockedFile.truncate(location));
|
||||
}
|
||||
|
||||
/**
|
||||
* Append the passed data to the end of the file.
|
||||
*
|
||||
* @param {string|ArrayBuffer} data
|
||||
* The data to append to the end of the file.
|
||||
*
|
||||
* @returns {Promise}
|
||||
* A promise which is resolved once the request has been completed.
|
||||
*/
|
||||
async append(data) {
|
||||
this.ensureLocked({ invalidMode: "readonly" });
|
||||
return waitForDOMRequest(this.lockedFile.append(data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Write data into the file (optionally starting from a defined location in the file).
|
||||
*
|
||||
* @param {string|ArrayBuffer} data
|
||||
* The data to write into the file.
|
||||
* @param {number} location
|
||||
* The location where the data should be written.
|
||||
*
|
||||
* @returns {Promise<number>}
|
||||
* A promise which is resolved to the location where the written data ends.
|
||||
*/
|
||||
async write(data, location) {
|
||||
this.ensureLocked({ invalidMode: "readonly" });
|
||||
if (typeof location === "number") {
|
||||
this.lockedFile.location = location;
|
||||
}
|
||||
return waitForDOMRequest(this.lockedFile.write(data),
|
||||
// Resolves to the new location.
|
||||
() => {
|
||||
return this.lockedFile.location;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue data to be written into the file (optionally starting from a defined location in the file).
|
||||
*
|
||||
* @param {string|ArrayBuffer} data
|
||||
* The data to write into the file.
|
||||
* @param {number} location
|
||||
* The location where the data should be written (when not specified the end of the previous
|
||||
* queued write is used).
|
||||
*
|
||||
* @returns {Promise<number>}
|
||||
* A promise which is resolved once the request has been completed with the location where the
|
||||
* file was after the data has been writted.
|
||||
*/
|
||||
queuedWrite(data, location) {
|
||||
const nextWriteRequest = async lastLocation => {
|
||||
this.ensureLocked({ invalidMode: "readonly" });
|
||||
|
||||
if (typeof location === "number") {
|
||||
return this.write(data, location);
|
||||
}
|
||||
return this.write(data, lastLocation);
|
||||
};
|
||||
|
||||
this.writeQueue = this.writeQueue.then(nextWriteRequest);
|
||||
return this.writeQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait that any queued data has been written.
|
||||
*
|
||||
* @returns {Promise<number>}
|
||||
* A promise which is resolved once the request has been completed with the location where the
|
||||
* file was after the data has been writted.
|
||||
*/
|
||||
async waitForQueuedWrites() {
|
||||
await this.writeQueue;
|
||||
}
|
||||
}
|
||||
|
||||
exports.IDBPromisedFileHandle = IDBPromisedFileHandle;
|
||||
/**
|
||||
* Wraps an IDBMutableFile with a nicer Promise-based API.
|
||||
*
|
||||
* Instances of this class are created from the
|
||||
* {@link IDBFileStorage.createMutableFile} method.
|
||||
*/
|
||||
class IDBPromisedMutableFile {
|
||||
/**
|
||||
* @private private helper method used internally.
|
||||
*/
|
||||
constructor({ filesStorage, idb, fileName, fileType, mutableFile }) {
|
||||
// All the following properties are private and it should not be needed
|
||||
// while using the API.
|
||||
|
||||
/** @private */
|
||||
this.filesStorage = filesStorage;
|
||||
/** @private */
|
||||
this.idb = idb;
|
||||
/** @private */
|
||||
this.fileName = fileName;
|
||||
/** @private */
|
||||
this.fileType = fileType;
|
||||
/** @private */
|
||||
this.mutableFile = mutableFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private private helper method used internally.
|
||||
*/
|
||||
reopenFileHandle(fileHandle) {
|
||||
fileHandle.lockedFile = this.mutableFile.open(fileHandle.mode);
|
||||
}
|
||||
|
||||
// API methods.
|
||||
|
||||
/**
|
||||
* Open a mutable file for reading/writing data.
|
||||
*
|
||||
* @param {"readonly"|"readwrite"|"writeonly"} mode
|
||||
* The mode of the created IDBPromisedFileHandle instance.
|
||||
*
|
||||
* @returns {IDBPromisedFileHandle}
|
||||
* The created IDBPromisedFileHandle instance.
|
||||
*/
|
||||
open(mode) {
|
||||
if (this.lockedFile) {
|
||||
throw new Error("MutableFile cannot be opened twice");
|
||||
}
|
||||
const lockedFile = this.mutableFile.open(mode);
|
||||
|
||||
return new IDBPromisedFileHandle({ file: this, lockedFile });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link File} instance of this mutable file.
|
||||
*
|
||||
* @returns {Promise<File>}
|
||||
* A promise resolved to the File instance.
|
||||
*
|
||||
* To read the actual content of the mutable file as a File object,
|
||||
* it is often better to use {@link IDBPromisedMutableFile.saveAsFileSnapshot}
|
||||
* to save a persistent snapshot of the file in the IndexedDB store,
|
||||
* or reading it directly using the {@link IDBPromisedFileHandle} instance
|
||||
* returned by the {@link IDBPromisedMutableFile.open} method.
|
||||
*
|
||||
* The reason is that to be able to read the content of the returned file
|
||||
* a lockfile have be keep the file open, e.d. as in the following example.
|
||||
*
|
||||
* @example
|
||||
* ...
|
||||
* let waitSnapshotStored;
|
||||
* await mutableFile.runFileRequestGenerator(function* (lockedFile) {
|
||||
* const file = yield lockedFile.mutableFile.getFile();
|
||||
* // read the file content or turn it into a persistent object of its own
|
||||
* // (e.g. by saving it back into IndexedDB as its snapshot in form of a File object,
|
||||
* // or converted into a data url, a string or an array buffer)
|
||||
*
|
||||
* waitSnapshotStored = tmpFiles.put("${filename}/last_snapshot", file);
|
||||
* }
|
||||
*
|
||||
* await waitSnapshotStored;
|
||||
* let fileSnapshot = await tmpFiles.get("${filename}/last_snapshot");
|
||||
* ...
|
||||
* // now you can use fileSnapshot even if the mutableFile lock is not active anymore.
|
||||
*/
|
||||
getFile() {
|
||||
return waitForDOMRequest(this.mutableFile.getFile());
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the content of the mutable file into the files storage
|
||||
* as a File, using the specified snapshot name and return the persisted File instance.
|
||||
*
|
||||
* @returns {Promise<File>}
|
||||
* A promise resolved to the File instance.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* const file = await mutableFile.persistAsFileSnapshot(`${filename}/last_snapshot`);
|
||||
* const blobURL = URL.createObjectURL(file);
|
||||
* ...
|
||||
* // The blob URL is still valid even if the mutableFile is not active anymore.
|
||||
*/
|
||||
async persistAsFileSnapshot(snapshotName) {
|
||||
if (snapshotName === this.fileName) {
|
||||
throw new Error("Snapshot name and the file name should be different");
|
||||
}
|
||||
|
||||
const idb = await this.filesStorage.initializedDB();
|
||||
await this.runFileRequestGenerator(function* () {
|
||||
const file = yield this.mutableFile.getFile();
|
||||
const objectStore = this.filesStorage.getObjectStoreTransaction({ idb, mode: "readwrite" });
|
||||
|
||||
yield objectStore.put(file, snapshotName);
|
||||
}.bind(this));
|
||||
|
||||
return this.filesStorage.get(snapshotName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the this mutable file into its related IDBFileStorage.
|
||||
*
|
||||
* @returns {Promise}
|
||||
* A promise resolved on the mutable file has been persisted into IndexedDB.
|
||||
*/
|
||||
persist() {
|
||||
return this.filesStorage.put(this.fileName, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a generator function which can run a sequence of FileRequests
|
||||
* without the lockfile to become inactive.
|
||||
*
|
||||
* This method should be rarely needed, mostly to optimize a sequence of
|
||||
* file operations without the file to be closed and automatically re-opened
|
||||
* between two file requests.
|
||||
*
|
||||
* @param {function* (lockedFile) {...}} generatorFunction
|
||||
* @param {"readonly"|"readwrite"|"writeonly"} mode
|
||||
*
|
||||
* @example
|
||||
* (async function () {
|
||||
* const tmpFiles = await IDBFiles.getFileStorage({name: "tmpFiles"});
|
||||
* const mutableFile = await tmpFiles.createMutableFile("test-mutable-file.txt");
|
||||
*
|
||||
* let allFileData;
|
||||
*
|
||||
* function* fileOperations(lockedFile) {
|
||||
* yield lockedFile.write("some data");
|
||||
* yield lockedFile.write("more data");
|
||||
* const metadata = yield lockedFile.getMetadata();
|
||||
*
|
||||
* lockedFile.location = 0;
|
||||
* allFileData = yield lockedFile.readAsText(metadata.size);
|
||||
* }
|
||||
*
|
||||
* await mutableFile.runFileRequestGenerator(fileOperations, "readwrite");
|
||||
*
|
||||
* console.log("File Data", allFileData);
|
||||
* })();
|
||||
*/
|
||||
async runFileRequestGenerator(generatorFunction, mode) {
|
||||
if (generatorFunction.constructor.name !== "GeneratorFunction") {
|
||||
throw new Error("runGenerator parameter should be a generator function");
|
||||
}
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const lockedFile = this.mutableFile.open(mode || "readwrite");
|
||||
const fileRequestsIter = generatorFunction(lockedFile);
|
||||
|
||||
const processFileRequestIter = prevRequestResult => {
|
||||
const nextFileRequest = fileRequestsIter.next(prevRequestResult);
|
||||
if (nextFileRequest.done) {
|
||||
resolve();
|
||||
return;
|
||||
} else if (!(nextFileRequest.value instanceof window.DOMRequest || nextFileRequest.value instanceof window.IDBRequest)) {
|
||||
const error = new Error("FileRequestGenerator should only yield DOMRequest instances");
|
||||
fileRequestsIter.throw(error);
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const request = nextFileRequest.value;
|
||||
if (request.onsuccess || request.onerror) {
|
||||
const error = new Error("DOMRequest onsuccess/onerror callbacks are already set");
|
||||
fileRequestsIter.throw(error);
|
||||
reject(error);
|
||||
} else {
|
||||
request.onsuccess = () => processFileRequestIter(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
}
|
||||
};
|
||||
|
||||
processFileRequestIter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
exports.IDBPromisedMutableFile = IDBPromisedMutableFile;
|
||||
/**
|
||||
* Provides a Promise-based API to store files into an IndexedDB.
|
||||
*
|
||||
* Instances of this class are created using the exported
|
||||
* {@link getFileStorage} function.
|
||||
*/
|
||||
class IDBFileStorage {
|
||||
/**
|
||||
* @private private helper method used internally.
|
||||
*/
|
||||
constructor({ name, persistent } = {}) {
|
||||
// All the following properties are private and it should not be needed
|
||||
// while using the API.
|
||||
|
||||
/** @private */
|
||||
this.name = name;
|
||||
/** @private */
|
||||
this.persistent = persistent;
|
||||
/** @private */
|
||||
this.indexedDBName = `IDBFilesStorage-DB-${this.name}`;
|
||||
/** @private */
|
||||
this.objectStorageName = "IDBFilesObjectStorage";
|
||||
/** @private */
|
||||
this.initializedPromise = undefined;
|
||||
|
||||
// TODO: evalutate schema migration between library versions?
|
||||
/** @private */
|
||||
this.version = 1.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private private helper method used internally.
|
||||
*/
|
||||
initializedDB() {
|
||||
if (this.initializedPromise) {
|
||||
return this.initializedPromise;
|
||||
}
|
||||
|
||||
this.initializedPromise = (async () => {
|
||||
if (window.IDBMutableFile && this.persistent) {
|
||||
this.version = { version: this.version, storage: "persistent" };
|
||||
}
|
||||
const dbReq = indexedDB.open(this.indexedDBName, this.version);
|
||||
|
||||
dbReq.onupgradeneeded = () => {
|
||||
const db = dbReq.result;
|
||||
if (!db.objectStoreNames.contains(this.objectStorageName)) {
|
||||
db.createObjectStore(this.objectStorageName);
|
||||
}
|
||||
};
|
||||
|
||||
return waitForDOMRequest(dbReq);
|
||||
})();
|
||||
|
||||
return this.initializedPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private private helper method used internally.
|
||||
*/
|
||||
getObjectStoreTransaction({ idb, mode } = {}) {
|
||||
const transaction = idb.transaction([this.objectStorageName], mode);
|
||||
return transaction.objectStore(this.objectStorageName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new IDBPromisedMutableFile instance (where the IDBMutableFile is supported)
|
||||
*
|
||||
* @param {string} fileName
|
||||
* The fileName associated to the new IDBPromisedMutableFile instance.
|
||||
* @param {string} [fileType="text"]
|
||||
* The mime type associated to the file.
|
||||
*
|
||||
* @returns {IDBPromisedMutableFile}
|
||||
* The newly created {@link IDBPromisedMutableFile} instance.
|
||||
*/
|
||||
async createMutableFile(fileName, fileType = "text") {
|
||||
if (!window.IDBMutableFile) {
|
||||
throw new Error("This environment does not support IDBMutableFile");
|
||||
}
|
||||
const idb = await this.initializedDB();
|
||||
const mutableFile = await waitForDOMRequest(idb.createMutableFile(fileName, fileType));
|
||||
return new IDBPromisedMutableFile({
|
||||
filesStorage: this, idb, fileName, fileType, mutableFile
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Put a file object into the IDBFileStorage, it overwrites an existent file saved with the
|
||||
* fileName if any.
|
||||
*
|
||||
* @param {string} fileName
|
||||
* The key associated to the file in the IDBFileStorage.
|
||||
* @param {Blob|File|IDBPromisedMutableFile|IDBMutableFile} file
|
||||
* The file to be persisted.
|
||||
*
|
||||
* @returns {Promise}
|
||||
* A promise resolved when the request has been completed.
|
||||
*/
|
||||
async put(fileName, file) {
|
||||
if (!fileName || typeof fileName !== "string") {
|
||||
throw new Error("fileName parameter is mandatory");
|
||||
}
|
||||
|
||||
if (!(file instanceof File) && !(file instanceof Blob) && !(window.IDBMutableFile && file instanceof window.IDBMutableFile) && !(file instanceof IDBPromisedMutableFile)) {
|
||||
throw new Error(`Unable to persist ${fileName}. Unknown file type.`);
|
||||
}
|
||||
|
||||
if (file instanceof IDBPromisedMutableFile) {
|
||||
file = file.mutableFile;
|
||||
}
|
||||
|
||||
const idb = await this.initializedDB();
|
||||
const objectStore = this.getObjectStoreTransaction({ idb, mode: "readwrite" });
|
||||
return waitForDOMRequest(objectStore.put(file, fileName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a file object from the IDBFileStorage.
|
||||
*
|
||||
* @param {string} fileName
|
||||
* The fileName (the associated IndexedDB key) to remove from the IDBFileStorage.
|
||||
*
|
||||
* @returns {Promise}
|
||||
* A promise resolved when the request has been completed.
|
||||
*/
|
||||
async remove(fileName) {
|
||||
if (!fileName) {
|
||||
throw new Error("fileName parameter is mandatory");
|
||||
}
|
||||
|
||||
const idb = await this.initializedDB();
|
||||
const objectStore = this.getObjectStoreTransaction({ idb, mode: "readwrite" });
|
||||
return waitForDOMRequest(objectStore.delete(fileName));
|
||||
}
|
||||
|
||||
/**
|
||||
* List the names of the files stored in the IDBFileStorage.
|
||||
*
|
||||
* (If any filtering options has been specified, only the file names that match
|
||||
* all the filters are included in the result).
|
||||
*
|
||||
* @param {IDBFileStorage.ListFilteringOptions} options
|
||||
* The optional filters to apply while listing the stored file names.
|
||||
*
|
||||
* @returns {Promise<string[]>}
|
||||
* A promise resolved to the array of the filenames that has been found.
|
||||
*/
|
||||
async list(options) {
|
||||
const idb = await this.initializedDB();
|
||||
const objectStore = this.getObjectStoreTransaction({ idb });
|
||||
const allKeys = await waitForDOMRequest(objectStore.getAllKeys());
|
||||
|
||||
let filteredKeys = allKeys;
|
||||
|
||||
if (options) {
|
||||
filteredKeys = filteredKeys.filter(key => {
|
||||
let match = true;
|
||||
|
||||
if (typeof options.startsWith === "string") {
|
||||
match = match && key.startsWith(options.startsWith);
|
||||
}
|
||||
|
||||
if (typeof options.endsWith === "string") {
|
||||
match = match && key.endsWith(options.endsWith);
|
||||
}
|
||||
|
||||
if (typeof options.includes === "string") {
|
||||
match = match && key.includes(options.includes);
|
||||
}
|
||||
|
||||
if (typeof options.filterFn === "function") {
|
||||
match = match && options.filterFn(key);
|
||||
}
|
||||
|
||||
return match;
|
||||
});
|
||||
}
|
||||
|
||||
return filteredKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of files stored in the IDBFileStorage.
|
||||
*
|
||||
* (If any filtering options has been specified, only the file names that match
|
||||
* all the filters are included in the final count).
|
||||
*
|
||||
* @param {IDBFileStorage.ListFilteringOptions} options
|
||||
* The optional filters to apply while listing the stored file names.
|
||||
*
|
||||
* @returns {Promise<number>}
|
||||
* A promise resolved to the number of files that has been found.
|
||||
*/
|
||||
async count(options) {
|
||||
if (!options) {
|
||||
const idb = await this.initializedDB();
|
||||
const objectStore = this.getObjectStoreTransaction({ idb });
|
||||
return waitForDOMRequest(objectStore.count());
|
||||
}
|
||||
|
||||
const filteredKeys = await this.list(options);
|
||||
return filteredKeys.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a file stored in the IDBFileStorage by key.
|
||||
*
|
||||
* @param {string} fileName
|
||||
* The key to use to retrieve the file from the IDBFileStorage.
|
||||
*
|
||||
* @returns {Promise<Blob|File|IDBPromisedMutableFile>}
|
||||
* A promise resolved once the file stored in the IDBFileStorage has been retrieved.
|
||||
*/
|
||||
async get(fileName) {
|
||||
const idb = await this.initializedDB();
|
||||
const objectStore = this.getObjectStoreTransaction({ idb });
|
||||
return waitForDOMRequest(objectStore.get(fileName)).then(result => {
|
||||
if (window.IDBMutableFile && result instanceof window.IDBMutableFile) {
|
||||
return new IDBPromisedMutableFile({
|
||||
filesStorage: this,
|
||||
idb,
|
||||
fileName,
|
||||
fileType: result.type,
|
||||
mutableFile: result
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all the file objects stored in the IDBFileStorage.
|
||||
*
|
||||
* @returns {Promise}
|
||||
* A promise resolved once the IDBFileStorage has been cleared.
|
||||
*/
|
||||
async clear() {
|
||||
const idb = await this.initializedDB();
|
||||
const objectStore = this.getObjectStoreTransaction({ idb, mode: "readwrite" });
|
||||
return waitForDOMRequest(objectStore.clear());
|
||||
}
|
||||
}
|
||||
|
||||
exports.IDBFileStorage = IDBFileStorage;
|
||||
/**
|
||||
* Retrieve an IDBFileStorage instance by name (and it creates the indexedDB if it doesn't
|
||||
* exist yet).
|
||||
*
|
||||
* @param {Object} [param]
|
||||
* @param {string} [param.name="default"]
|
||||
* The name associated to the IDB File Storage.
|
||||
* @param {boolean} [param.persistent]
|
||||
* Optionally enable persistent storage mode (not enabled by default).
|
||||
*
|
||||
* @returns {IDBFileStorage}
|
||||
* The IDBFileStorage instance with the given name.
|
||||
*/
|
||||
async function getFileStorage({ name, persistent } = {}) {
|
||||
const filesStorage = new IDBFileStorage({ name: name || "default", persistent });
|
||||
await filesStorage.initializedDB();
|
||||
return filesStorage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @external {Blob} https://developer.mozilla.org/en-US/docs/Web/API/Blob
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external {DOMRequest} https://developer.mozilla.org/en/docs/Web/API/DOMRequest
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external {File} https://developer.mozilla.org/en-US/docs/Web/API/File
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external {IDBMutableFile} https://developer.mozilla.org/en-US/docs/Web/API/IDBMutableFile
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external {IDBRequest} https://developer.mozilla.org/en-US/docs/Web/API/IDBRequest
|
||||
*/
|
||||
});
|
||||
//# sourceMappingURL=idb-file-storage.js.map
|
||||
1
store-collected-images/webextension-plain/deps/uuidv4.js
Normal file
@@ -0,0 +1 @@
|
||||
!function(n){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=n();else if("function"==typeof define&&define.amd)define([],n);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.uuidv4=n()}}(function(){return function n(e,r,o){function t(f,u){if(!r[f]){if(!e[f]){var a="function"==typeof require&&require;if(!u&&a)return a(f,!0);if(i)return i(f,!0);var d=new Error("Cannot find module '"+f+"'");throw d.code="MODULE_NOT_FOUND",d}var l=r[f]={exports:{}};e[f][0].call(l.exports,function(n){var r=e[f][1][n];return t(r?r:n)},l,l.exports,n,e,r,o)}return r[f].exports}for(var i="function"==typeof require&&require,f=0;f<o.length;f++)t(o[f]);return t}({1:[function(n,e,r){function o(n,e){var r=e||0,o=t;return o[n[r++]]+o[n[r++]]+o[n[r++]]+o[n[r++]]+"-"+o[n[r++]]+o[n[r++]]+"-"+o[n[r++]]+o[n[r++]]+"-"+o[n[r++]]+o[n[r++]]+"-"+o[n[r++]]+o[n[r++]]+o[n[r++]]+o[n[r++]]+o[n[r++]]+o[n[r++]]}for(var t=[],i=0;i<256;++i)t[i]=(i+256).toString(16).substr(1);e.exports=o},{}],2:[function(n,e,r){(function(n){var r,o=n.crypto||n.msCrypto;if(o&&o.getRandomValues){var t=new Uint8Array(16);r=function(){return o.getRandomValues(t),t}}if(!r){var i=new Array(16);r=function(){for(var n,e=0;e<16;e++)0===(3&e)&&(n=4294967296*Math.random()),i[e]=n>>>((3&e)<<3)&255;return i}}e.exports=r}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],3:[function(n,e,r){function o(n,e,r){var o=e&&r||0;"string"==typeof n&&(e="binary"==n?new Array(16):null,n=null),n=n||{};var f=n.random||(n.rng||t)();if(f[6]=15&f[6]|64,f[8]=63&f[8]|128,e)for(var u=0;u<16;++u)e[o+u]=f[u];return e||i(f)}var t=n("./lib/rng"),i=n("./lib/bytesToUuid");e.exports=o},{"./lib/bytesToUuid":1,"./lib/rng":2}]},{},[3])(3)});
|
||||
BIN
store-collected-images/webextension-plain/images/icon.png
Normal file
|
After Width: | Height: | Size: 695 B |
BIN
store-collected-images/webextension-plain/images/icon16.png
Normal file
|
After Width: | Height: | Size: 1008 B |
26
store-collected-images/webextension-plain/manifest.json
Executable file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "store-collected-images",
|
||||
"version": "1.0",
|
||||
|
||||
"icons": {
|
||||
"16": "images/icon16.png",
|
||||
"48": "images/icon.png"
|
||||
},
|
||||
|
||||
"browser_action": {
|
||||
"default_icon": {
|
||||
"48": "images/icon.png"
|
||||
},
|
||||
"default_title": "Collected Images"
|
||||
},
|
||||
|
||||
"background": {
|
||||
"scripts": ["background.js"]
|
||||
},
|
||||
|
||||
"permissions": [
|
||||
"contextMenus",
|
||||
"<all_urls>"
|
||||
]
|
||||
}
|
||||
1
store-collected-images/webextension-plain/navigate-collection.css
Executable file
@@ -0,0 +1 @@
|
||||
@import "shared.css";
|
||||
21
store-collected-images/webextension-plain/navigate-collection.html
Executable file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="navigate-collection.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<h3>Stored images</h3>
|
||||
<input placeholder="filter image" class="image-filter">
|
||||
<button class="reload-images">reload</button>
|
||||
<button class="delete-images">delete</button>
|
||||
<ul class="thumbnails"></ul>
|
||||
</div>
|
||||
<script src="/deps/idb-file-storage.js"></script>
|
||||
<script src="/deps/uuidv4.js"></script>
|
||||
<script src="/utils/handle-window-drag-and-drop.js"></script>
|
||||
<script src="/utils/image-store.js"></script>
|
||||
<script src="navigate-collection.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,85 @@
|
||||
/* global loadStoredImages, removeStoredImages */
|
||||
|
||||
"use strict";
|
||||
|
||||
class NavigateCollectionUI {
|
||||
constructor(containerEl) {
|
||||
this.containerEl = containerEl;
|
||||
|
||||
this.state = {
|
||||
storedImages: [],
|
||||
};
|
||||
|
||||
this.onFilterUpdated = this.onFilterUpdated.bind(this);
|
||||
this.onReload = this.onFilterUpdated;
|
||||
this.onDelete = this.onDelete.bind(this);
|
||||
|
||||
this.containerEl.querySelector("button.reload-images").onclick = this.onReload;
|
||||
this.containerEl.querySelector("button.delete-images").onclick = this.onDelete;
|
||||
this.containerEl.querySelector("input.image-filter").onchange = this.onFilterUpdated;
|
||||
|
||||
// Load the stored image once the component has been rendered in the page.
|
||||
this.onFilterUpdated();
|
||||
}
|
||||
|
||||
get imageFilterValue() {
|
||||
return this.containerEl.querySelector("input.image-filter").value;
|
||||
}
|
||||
|
||||
set imageFilterValue(value) {
|
||||
return this.containerEl.querySelector("input.image-filter").value = value;
|
||||
}
|
||||
|
||||
setState(state) {
|
||||
// Merge the new state on top of the previous one and re-render everything.
|
||||
this.state = Object.assign(this.state, state);
|
||||
this.render();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Load the stored image once the component has been rendered in the page.
|
||||
this.onFilterUpdated();
|
||||
}
|
||||
|
||||
onFilterUpdated() {
|
||||
loadStoredImages(this.imageFilterValue)
|
||||
.then((storedImages) => {
|
||||
this.setState({storedImages});
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
onDelete() {
|
||||
const {storedImages} = this.state;
|
||||
this.setState({storedImages: []});
|
||||
|
||||
removeStoredImages(storedImages).catch(console.error);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {storedImages} = this.state;
|
||||
|
||||
const thumbnailsUl = this.containerEl.querySelector("ul.thumbnails");
|
||||
while (thumbnailsUl.firstChild) {
|
||||
thumbnailsUl.removeChild(thumbnailsUl.firstChild);
|
||||
}
|
||||
|
||||
storedImages.forEach(({storedName, blobUrl}) => {
|
||||
const onClickedImage = () => {
|
||||
this.imageFilterValue = storedName;
|
||||
this.onFilterUpdated();
|
||||
};
|
||||
const li = document.createElement("li");
|
||||
const img = document.createElement("img");
|
||||
li.setAttribute("id", storedName);
|
||||
img.setAttribute("src", blobUrl);
|
||||
img.onclick = onClickedImage;
|
||||
|
||||
li.appendChild(img);
|
||||
thumbnailsUl.appendChild(li);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const navigateCollectionUI = new NavigateCollectionUI(document.getElementById('app'));
|
||||
12
store-collected-images/webextension-plain/popup.css
Executable file
@@ -0,0 +1,12 @@
|
||||
@import "shared.css";
|
||||
|
||||
html, body {
|
||||
width: 250px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 90%;
|
||||
}
|
||||
22
store-collected-images/webextension-plain/popup.html
Executable file
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="popup.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<h3>Collected images</h3>
|
||||
<p id="error-message"></p>
|
||||
<input placeholder="collection name" class="collection-name">
|
||||
<button class="save-collection">save</button>
|
||||
<ul class="thumbnails">
|
||||
<ul>
|
||||
</div>
|
||||
<script src="/deps/idb-file-storage.js"></script>
|
||||
<script src="/deps/uuidv4.js"></script>
|
||||
<script src="/utils/handle-window-drag-and-drop.js"></script>
|
||||
<script src="/utils/image-store.js"></script>
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
127
store-collected-images/webextension-plain/popup.js
Normal file
@@ -0,0 +1,127 @@
|
||||
/* global saveCollectedBlobs, uuidv4, preventWindowDragAndDrop */
|
||||
|
||||
"use strict";
|
||||
|
||||
class Popup {
|
||||
constructor(containerEl) {
|
||||
this.containerEl = containerEl;
|
||||
|
||||
this.state = {
|
||||
collectedBlobs: [],
|
||||
lastMessage: undefined,
|
||||
};
|
||||
|
||||
this.onClick = this.onClick.bind(this);
|
||||
|
||||
this.containerEl.querySelector("button.save-collection").onclick = this.onClick;
|
||||
}
|
||||
|
||||
get collectionNameValue() {
|
||||
return this.containerEl.querySelector("input.collection-name").value;
|
||||
}
|
||||
|
||||
setState(state) {
|
||||
// Merge the new state on top of the previous one and re-render everything.
|
||||
this.state = Object.assign(this.state, state);
|
||||
this.render();
|
||||
}
|
||||
|
||||
onClick() {
|
||||
if (!this.collectionNameValue) {
|
||||
this.setState({
|
||||
lastMessage: {text: "The collection name is mandatory.", type: "error"},
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.setState({lastMessage: undefined});
|
||||
}, 2000);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
saveCollectedBlobs(this.collectionNameValue, this.state.collectedBlobs)
|
||||
.then(() => {
|
||||
this.setState({
|
||||
lastMessage: {text: "All the collected images have been saved", type: "success"},
|
||||
collectedBlobs: [],
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.setState({lastMessage: undefined});
|
||||
}, 2000);
|
||||
})
|
||||
.catch((err) => {
|
||||
this.setState({
|
||||
lastMessage: {text: `Failed to save collected images: ${err}`, type: "error"},
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.setState({lastMessage: undefined});
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {collectedBlobs, lastMessage} = this.state;
|
||||
|
||||
const lastMessageEl = this.containerEl.querySelector("p#error-message");
|
||||
if (lastMessage) {
|
||||
lastMessageEl.setAttribute("class", lastMessage.type);
|
||||
lastMessageEl.textContent = lastMessage.text;
|
||||
} else {
|
||||
lastMessageEl.setAttribute("class", "");
|
||||
lastMessageEl.textContent = "";
|
||||
}
|
||||
|
||||
const thumbnailsUl = this.containerEl.querySelector("ul.thumbnails");
|
||||
while (thumbnailsUl.firstChild) {
|
||||
thumbnailsUl.removeChild(thumbnailsUl.firstChild);
|
||||
}
|
||||
|
||||
collectedBlobs.forEach(({uuid, blobUrl}) => {
|
||||
const li = document.createElement("li");
|
||||
const img = document.createElement("img");
|
||||
li.setAttribute("id", uuid);
|
||||
img.setAttribute("src", blobUrl);
|
||||
li.appendChild(img);
|
||||
|
||||
thumbnailsUl.appendChild(li);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const popup = new Popup(document.getElementById('app'));
|
||||
|
||||
async function fetchBlobFromUrl(fetchUrl) {
|
||||
const res = await fetch(fetchUrl);
|
||||
const blob = await res.blob();
|
||||
|
||||
return {
|
||||
blob,
|
||||
blobUrl: URL.createObjectURL(blob),
|
||||
fetchUrl,
|
||||
uuid: uuidv4(),
|
||||
};
|
||||
}
|
||||
|
||||
preventWindowDragAndDrop();
|
||||
|
||||
browser.runtime.onMessage.addListener(async (msg) => {
|
||||
if (msg.type === "new-collected-images") {
|
||||
let collectedBlobs = popup.state.collectedBlobs || [];
|
||||
const fetchRes = await fetchBlobFromUrl(msg.url);
|
||||
collectedBlobs.push(fetchRes);
|
||||
popup.setState({collectedBlobs});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
browser.runtime.sendMessage({type: "get-pending-collected-urls"}).then(async res => {
|
||||
let collectedBlobs = popup.state.collectedBlobs || [];
|
||||
|
||||
for (const url of res) {
|
||||
const fetchRes = await fetchBlobFromUrl(url);
|
||||
collectedBlobs.push(fetchRes);
|
||||
popup.setState({collectedBlobs});
|
||||
}
|
||||
});
|
||||
22
store-collected-images/webextension-plain/shared.css
Normal file
@@ -0,0 +1,22 @@
|
||||
ul.thumbnails {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul.thumbnails li {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 0.4em;
|
||||
}
|
||||
|
||||
.thumbnails img {
|
||||
max-width: 50px;
|
||||
max-height: 50px;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: rgba(255,0,0,0.4);
|
||||
}
|
||||
|
||||
.success {
|
||||
background: rgba(0,255,0,0.4);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/* exported preventWindowDragAndDrop */
|
||||
|
||||
"use strict";
|
||||
|
||||
function preventWindowDragAndDrop() {
|
||||
function preventDefault(ev) {
|
||||
ev.preventDefault();
|
||||
return true;
|
||||
}
|
||||
|
||||
window.ondragover = preventDefault;
|
||||
window.ondragend = preventDefault;
|
||||
window.ondrop = preventDefault;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/* global IDBFiles */
|
||||
/* exported saveCollectedBlobs, loadStoredImages, removeStoredImages */
|
||||
|
||||
"use strict";
|
||||
|
||||
async function saveCollectedBlobs(collectionName, collectedBlobs) {
|
||||
const storedImages = await IDBFiles.getFileStorage({name: "stored-images"});
|
||||
|
||||
for (const item of collectedBlobs) {
|
||||
await storedImages.put(`${collectionName}/${item.uuid}`, item.blob);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStoredImages(filter) {
|
||||
const imagesStore = await IDBFiles.getFileStorage({name: "stored-images"});
|
||||
|
||||
let listOptions = filter ? {includes: filter} : undefined;
|
||||
const imagesList = await imagesStore.list(listOptions);
|
||||
|
||||
let storedImages = [];
|
||||
|
||||
for (const storedName of imagesList) {
|
||||
const blob = await imagesStore.get(storedName);
|
||||
|
||||
storedImages.push({storedName, blobUrl: URL.createObjectURL(blob)});
|
||||
}
|
||||
|
||||
return storedImages;
|
||||
}
|
||||
|
||||
async function removeStoredImages(storedImages) {
|
||||
const imagesStore = await IDBFiles.getFileStorage({name: "stored-images"});
|
||||
for (const storedImage of storedImages) {
|
||||
URL.revokeObjectURL(storedImage.blobUrl);
|
||||
await imagesStore.remove(storedImage.storedName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parser": "babel-eslint",
|
||||
"env": {
|
||||
"commonjs": true
|
||||
}
|
||||
}
|
||||
5
store-collected-images/webextension-with-webpack/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# Ignore build artifacts and other files.
|
||||
.DS_Store
|
||||
yarn.lock
|
||||
extension/dist
|
||||
node_modules
|
||||
32
store-collected-images/webextension-with-webpack/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# "Image Reference Collector" example built with webpack (and React UI)
|
||||
|
||||
## Usage
|
||||
|
||||
This example is built using Babel and Webpack, and so the transpiled bundles have to
|
||||
be built first:
|
||||
|
||||
you need to change into the example subdirectory and install all
|
||||
[NodeJS][nodejs] dependencies with [npm](http://npmjs.com/) or
|
||||
[yarn](https://yarnpkg.com/):
|
||||
|
||||
npm install
|
||||
|
||||
You can build the extension using:
|
||||
|
||||
npm run build
|
||||
|
||||
This creates the source bundles for the WebExtension in the `extension` subdirectory, and
|
||||
you can manually install the add-on on Firefox by loading the `extension` from the
|
||||
"about:debugging#addons" page.
|
||||
|
||||
You can also build the sources and start a new Firefox instance with the add-on installed
|
||||
in one command:
|
||||
|
||||
npm run start
|
||||
|
||||
To start a webpack instance that automatically rebuilds the add-on when
|
||||
you change the sources, in another shell window, you can run the following npm script:
|
||||
|
||||
npm run build:watch
|
||||
|
||||
While this npm script is running, any time you edit a file, it will be rebuilt automatically.
|
||||
|
After Width: | Height: | Size: 695 B |
|
After Width: | Height: | Size: 1008 B |
26
store-collected-images/webextension-with-webpack/extension/manifest.json
Executable file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "store-collected-images",
|
||||
"version": "1.0",
|
||||
|
||||
"icons": {
|
||||
"16": "images/icon16.png",
|
||||
"48": "images/icon.png"
|
||||
},
|
||||
|
||||
"browser_action": {
|
||||
"default_icon": {
|
||||
"48": "images/icon.png"
|
||||
},
|
||||
"default_title": "Collected Images"
|
||||
},
|
||||
|
||||
"background": {
|
||||
"scripts": ["dist/background.js"]
|
||||
},
|
||||
|
||||
"permissions": [
|
||||
"contextMenus",
|
||||
"<all_urls>"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@import "shared.css";
|
||||
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="navigate-collection.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="dist/navigate-collection.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
12
store-collected-images/webextension-with-webpack/extension/popup.css
Executable file
@@ -0,0 +1,12 @@
|
||||
@import "shared.css";
|
||||
|
||||
html, body {
|
||||
width: 250px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 90%;
|
||||
}
|
||||
11
store-collected-images/webextension-with-webpack/extension/popup.html
Executable file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="popup.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="dist/popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,22 @@
|
||||
ul.thumbnails {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul.thumbnails li {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 0.4em;
|
||||
}
|
||||
|
||||
.thumbnails img {
|
||||
max-width: 50px;
|
||||
max-height: 50px;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: rgba(255,0,0,0.4);
|
||||
}
|
||||
|
||||
.success {
|
||||
background: rgba(0,255,0,0.4);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "store-collected-images",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "webpack --display-error-details --progress --colors",
|
||||
"build:watch": "npm run build -- -w",
|
||||
"start": "npm run build && web-ext run -s extension/"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"babel-core": "6.24.1",
|
||||
"babel-loader": "7.0.0",
|
||||
"babel-plugin-transform-class-properties": "6.24.1",
|
||||
"babel-plugin-transform-object-rest-spread": "6.23.0",
|
||||
"babel-plugin-transform-es2015-modules-commonjs": "6.24.1",
|
||||
"babel-preset-es2017": "6.24.1",
|
||||
"babel-preset-react": "6.24.1",
|
||||
"idb-file-storage": "^0.1.0",
|
||||
"react": "15.5.4",
|
||||
"react-dom": "15.5.4",
|
||||
"uuid": "^3.0.1",
|
||||
"web-ext": "1.9.1",
|
||||
"webpack": "2.6.1"
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
"es2017",
|
||||
"react"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-class-properties",
|
||||
"transform-es2015-modules-commonjs"
|
||||
]
|
||||
}
|
||||
}
|
||||