diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..a0612c7 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +**/node_modules/** +react-es6-popup/**/dist +mocha-client-tests +store-collected-images/webextension-plain/deps diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..bd5191f --- /dev/null +++ b/.eslintrc.json @@ -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"] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6892e2f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,3 @@ +language: node_js +node_js: stable +sudo: false diff --git a/README.md b/README.md index 505ef63..426b57a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# webextensions-examples +# webextensions-examples [![Build Status](https://travis-ci.org/mdn/webextensions-examples.svg?branch=master)](https://travis-ci.org/mdn/webextensions-examples) [https://github.com/mdn/webextensions-examples](https://github.com/mdn/webextensions-examples) diff --git a/annotate-page/sidebar/panel.js b/annotate-page/sidebar/panel.js index f4fdb46..2af8505 100644 --- a/annotate-page/sidebar/panel.js +++ b/annotate-page/sidebar/panel.js @@ -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 = {}; diff --git a/apply-css/background.js b/apply-css/background.js index 7e936a0..3c0b1aa 100644 --- a/apply-css/background.js +++ b/apply-css/background.js @@ -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); } }); diff --git a/bookmark-it/background.js b/bookmark-it/background.js index 9cc9a48..565fa06 100644 --- a/bookmark-it/background.js +++ b/bookmark-it/background.js @@ -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 + }); } /* diff --git a/chill-out/background.js b/chill-out/background.js index bbf453f..d29d949 100644 --- a/chill-out/background.js +++ b/chill-out/background.js @@ -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}); }); diff --git a/commands/README.md b/commands/README.md index 9cb9cc8..db36734 100644 --- a/commands/README.md +++ b/commands/README.md @@ -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. diff --git a/commands/background.js b/commands/background.js index 825c559..a54dc95 100644 --- a/commands/background.js +++ b/commands/background.js @@ -12,7 +12,7 @@ */ var gettingAllCommands = browser.commands.getAll(); gettingAllCommands.then((commands) => { - for (command of commands) { + for (let command of commands) { console.log(command); } }); diff --git a/context-menu-copy-link-with-types/background.js b/context-menu-copy-link-with-types/background.js index b8d1505..9152745 100644 --- a/context-menu-copy-link-with-types/background.js +++ b/context-menu-copy-link-with-types/background.js @@ -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); diff --git a/devtools-panels/README.md b/devtools-panels/README.md new file mode 100644 index 0000000..2cbcc30 --- /dev/null +++ b/devtools-panels/README.md @@ -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. diff --git a/devtools-panels/background_scripts/background.js b/devtools-panels/background_scripts/background.js new file mode 100644 index 0000000..18297fe --- /dev/null +++ b/devtools-panels/background_scripts/background.js @@ -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); diff --git a/devtools-panels/devtools/devtools-page.html b/devtools-panels/devtools/devtools-page.html new file mode 100644 index 0000000..807bd94 --- /dev/null +++ b/devtools-panels/devtools/devtools-page.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/devtools-panels/devtools/devtools.js b/devtools-panels/devtools/devtools.js new file mode 100644 index 0000000..ae6bf47 --- /dev/null +++ b/devtools-panels/devtools/devtools.js @@ -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); +}); diff --git a/devtools-panels/devtools/panel/devtools-panel.js b/devtools-panels/devtools/panel/devtools-panel.js new file mode 100644 index 0000000..5a1f35b --- /dev/null +++ b/devtools-panels/devtools/panel/devtools-panel.js @@ -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 + }); +}); diff --git a/devtools-panels/devtools/panel/panel.html b/devtools-panels/devtools/panel/panel.html new file mode 100644 index 0000000..7d71f21 --- /dev/null +++ b/devtools-panels/devtools/panel/panel.html @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/devtools-panels/icons/star.png b/devtools-panels/icons/star.png new file mode 100644 index 0000000..64c4e36 Binary files /dev/null and b/devtools-panels/icons/star.png differ diff --git a/devtools-panels/manifest.json b/devtools-panels/manifest.json new file mode 100644 index 0000000..4723098 --- /dev/null +++ b/devtools-panels/manifest.json @@ -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": [ + "" + ], + + "devtools_page": "devtools/devtools-page.html" + +} diff --git a/embedded-webextension-bootstrapped/.eslintrc.json b/embedded-webextension-bootstrapped/.eslintrc.json new file mode 100644 index 0000000..30e26e9 --- /dev/null +++ b/embedded-webextension-bootstrapped/.eslintrc.json @@ -0,0 +1,8 @@ +{ + "env": { + "browser": true, + "es6": true, + "amd": true, + "webextensions": true + } +} diff --git a/embedded-webextension-overlay/chrome.manifest b/embedded-webextension-overlay/chrome.manifest new file mode 100644 index 0000000..19c011e --- /dev/null +++ b/embedded-webextension-overlay/chrome.manifest @@ -0,0 +1,2 @@ +content my-overlay-addon content/ +overlay chrome://browser/content/browser.xul chrome://my-overlay-addon/content/overlay.xul diff --git a/embedded-webextension-overlay/content/init.js b/embedded-webextension-overlay/content/init.js new file mode 100644 index 0000000..6e3e62c --- /dev/null +++ b/embedded-webextension-overlay/content/init.js @@ -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` + ); + }); + }); +} diff --git a/embedded-webextension-overlay/content/overlay.xul b/embedded-webextension-overlay/content/overlay.xul new file mode 100644 index 0000000..debde2b --- /dev/null +++ b/embedded-webextension-overlay/content/overlay.xul @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/embedded-webextension-overlay/install.rdf b/embedded-webextension-overlay/install.rdf new file mode 100644 index 0000000..679a74b --- /dev/null +++ b/embedded-webextension-overlay/install.rdf @@ -0,0 +1,23 @@ + + + + + my-overlay-addon@me + 1.0.1 + My Legacy Overlay Addon + + 2 + true + + + + + {ec8030f7-c20a-464f-9b0e-13a3a9e97384} + 51.0 + * + + + + + \ No newline at end of file diff --git a/embedded-webextension-overlay/webextension/background.js b/embedded-webextension-overlay/webextension/background.js new file mode 100644 index 0000000..4e68cfc --- /dev/null +++ b/embedded-webextension-overlay/webextension/background.js @@ -0,0 +1,3 @@ +console.log("Embedded WebExtension", window.location.href); + +browser.runtime.sendMessage("embedded_webext -> overlay addon container"); diff --git a/embedded-webextension-overlay/webextension/manifest.json b/embedded-webextension-overlay/webextension/manifest.json new file mode 100644 index 0000000..ff4eb0f --- /dev/null +++ b/embedded-webextension-overlay/webextension/manifest.json @@ -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"] + } +} diff --git a/embedded-webextension-sdk/.eslintrc.json b/embedded-webextension-sdk/.eslintrc.json new file mode 100644 index 0000000..30e26e9 --- /dev/null +++ b/embedded-webextension-sdk/.eslintrc.json @@ -0,0 +1,8 @@ +{ + "env": { + "browser": true, + "es6": true, + "amd": true, + "webextensions": true + } +} diff --git a/emoji-substitution/emojiMap.js b/emoji-substitution/emojiMap.js index 16c689e..b2f1b9e 100644 --- a/emoji-substitution/emojiMap.js +++ b/emoji-substitution/emojiMap.js @@ -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', '🍌'); diff --git a/emoji-substitution/substitute.js b/emoji-substitution/substitute.js index c886063..39203db 100644 --- a/emoji-substitution/substitute.js +++ b/emoji-substitution/substitute.js @@ -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; diff --git a/examples.json b/examples.json index d579583..31ad03a 100644 --- a/examples.json +++ b/examples.json @@ -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:" + }, { "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": [ diff --git a/google-userinfo/background/authorize.js b/google-userinfo/background/authorize.js index 633c41b..3b349c4 100644 --- a/google-userinfo/background/authorize.js +++ b/google-userinfo/background/authorize.js @@ -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]); diff --git a/google-userinfo/background/main.js b/google-userinfo/background/main.js index ac70222..9ddf1fb 100644 --- a/google-userinfo/background/main.js +++ b/google-userinfo/background/main.js @@ -1,3 +1,5 @@ +/*global getAccessToken*/ + function notifyUser(user) { browser.notifications.create({ "type": "basic", diff --git a/google-userinfo/background/userinfo.js b/google-userinfo/background/userinfo.js index 84a5427..5f7ac1d 100644 --- a/google-userinfo/background/userinfo.js +++ b/google-userinfo/background/userinfo.js @@ -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(); diff --git a/history-deleter/history.js b/history-deleter/history.js index 11a1dff..798f8db 100644 --- a/history-deleter/history.js +++ b/history-deleter/history.js @@ -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. diff --git a/imagify/README.md b/imagify/README.md new file mode 100644 index 0000000..b5e0359 --- /dev/null +++ b/imagify/README.md @@ -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 diff --git a/imagify/content_scripts/content.js b/imagify/content_scripts/content.js new file mode 100644 index 0000000..1c83760 --- /dev/null +++ b/imagify/content_scripts/content.js @@ -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); + +})(); \ No newline at end of file diff --git a/imagify/manifest.json b/imagify/manifest.json new file mode 100644 index 0000000..93fcd5a --- /dev/null +++ b/imagify/manifest.json @@ -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", + "" + ], + + "sidebar_action": { + "default_title": "Imagify", + "default_panel": "sidebar/sidebar.html" + }, + + "web_accessible_resources": [ + "/viewer.html" + ] +} diff --git a/imagify/sidebar/choose_file.js b/imagify/sidebar/choose_file.js new file mode 100644 index 0000000..12780ea --- /dev/null +++ b/imagify/sidebar/choose_file.js @@ -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(); +} \ No newline at end of file diff --git a/imagify/sidebar/sidebar.css b/imagify/sidebar/sidebar.css new file mode 100644 index 0000000..27f3ca7 --- /dev/null +++ b/imagify/sidebar/sidebar.css @@ -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; +} \ No newline at end of file diff --git a/imagify/sidebar/sidebar.html b/imagify/sidebar/sidebar.html new file mode 100644 index 0000000..c15ef7f --- /dev/null +++ b/imagify/sidebar/sidebar.html @@ -0,0 +1,24 @@ + + + + + + + + + + + +
+ +
+ +
+ Drag an image file into this Drop Zone ... +
+ + + + + + diff --git a/imagify/viewer.css b/imagify/viewer.css new file mode 100644 index 0000000..00c0af5 --- /dev/null +++ b/imagify/viewer.css @@ -0,0 +1,8 @@ +html, body { + margin: 0; + padding: 0; +} + +img { + width: 100%; +} diff --git a/imagify/viewer.html b/imagify/viewer.html new file mode 100644 index 0000000..e48614f --- /dev/null +++ b/imagify/viewer.html @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/imagify/viewer.js b/imagify/viewer.js new file mode 100644 index 0000000..50d15cd --- /dev/null +++ b/imagify/viewer.js @@ -0,0 +1,3 @@ +const params = new URLSearchParams(window.location.search); +const imageBlobURL = params.get("blobURL"); +document.querySelector("img").setAttribute("src", imageBlobURL); diff --git a/list-cookies/cookies.js b/list-cookies/cookies.js index 74c5a84..db2fc8e 100644 --- a/list-cookies/cookies.js +++ b/list-cookies/cookies.js @@ -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
  • 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 diff --git a/context-menu-demo/README.md b/menu-demo/README.md similarity index 64% rename from context-menu-demo/README.md rename to menu-demo/README.md index 24294d9..ca79920 100644 --- a/context-menu-demo/README.md +++ b/menu-demo/README.md @@ -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 diff --git a/context-menu-demo/_locales/en/messages.json b/menu-demo/_locales/en/messages.json similarity index 73% rename from context-menu-demo/_locales/en/messages.json rename to menu-demo/_locales/en/messages.json index deacbf3..709898f 100644 --- a/context-menu-demo/_locales/en/messages.json +++ b/menu-demo/_locales/en/messages.json @@ -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." } } diff --git a/context-menu-demo/background.js b/menu-demo/background.js similarity index 70% rename from context-menu-demo/background.js rename to menu-demo/background.js index 98f43a1..18b9d71 100644 --- a/context-menu-demo/background.js +++ b/menu-demo/background.js @@ -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; } }); diff --git a/context-menu-demo/icons/LICENSE b/menu-demo/icons/LICENSE similarity index 100% rename from context-menu-demo/icons/LICENSE rename to menu-demo/icons/LICENSE diff --git a/context-menu-demo/icons/page-16.png b/menu-demo/icons/page-16.png similarity index 100% rename from context-menu-demo/icons/page-16.png rename to menu-demo/icons/page-16.png diff --git a/context-menu-demo/icons/page-32.png b/menu-demo/icons/page-32.png similarity index 100% rename from context-menu-demo/icons/page-32.png rename to menu-demo/icons/page-32.png diff --git a/context-menu-demo/icons/page-48.png b/menu-demo/icons/page-48.png similarity index 100% rename from context-menu-demo/icons/page-48.png rename to menu-demo/icons/page-48.png diff --git a/context-menu-demo/manifest.json b/menu-demo/manifest.json similarity index 90% rename from context-menu-demo/manifest.json rename to menu-demo/manifest.json index 203cdae..f984c05 100644 --- a/context-menu-demo/manifest.json +++ b/menu-demo/manifest.json @@ -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" ], diff --git a/context-menu-demo/sidebar/sidebar.html b/menu-demo/sidebar/sidebar.html similarity index 100% rename from context-menu-demo/sidebar/sidebar.html rename to menu-demo/sidebar/sidebar.html diff --git a/mocha-client-tests/.eslintrc.json b/mocha-client-tests/.eslintrc.json new file mode 100644 index 0000000..30e26e9 --- /dev/null +++ b/mocha-client-tests/.eslintrc.json @@ -0,0 +1,8 @@ +{ + "env": { + "browser": true, + "es6": true, + "amd": true, + "webextensions": true + } +} diff --git a/mocha-client-tests/addon/background.js b/mocha-client-tests/addon/background.js index c434fcc..f356388 100644 --- a/mocha-client-tests/addon/background.js +++ b/mocha-client-tests/addon/background.js @@ -12,4 +12,4 @@ var Background = { } }; -chrome.runtime.onMessage.addListener(Background.receiveMessage); +browser.runtime.onMessage.addListener(Background.receiveMessage); diff --git a/mocha-client-tests/addon/scripts/popup.js b/mocha-client-tests/addon/scripts/popup.js index b352b5f..9e32f28 100644 --- a/mocha-client-tests/addon/scripts/popup.js +++ b/mocha-client-tests/addon/scripts/popup.js @@ -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; }); } diff --git a/package.json b/package.json new file mode 100644 index 0000000..a47007c --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/page-to-extension-messaging/content-script.js b/page-to-extension-messaging/content-script.js index 01646c9..418fa46 100644 --- a/page-to-extension-messaging/content-script.js +++ b/page-to-extension-messaging/content-script.js @@ -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") { diff --git a/permissions/.eslintrc.json b/permissions/.eslintrc.json new file mode 100644 index 0000000..22c3ada --- /dev/null +++ b/permissions/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "parserOptions": { + "ecmaVersion": 8 + } +} diff --git a/proxy-blocker/background/proxy-handler.js b/proxy-blocker/background/proxy-handler.js index 098089c..4a0ff0b 100644 --- a/proxy-blocker/background/proxy-handler.js +++ b/proxy-blocker/background/proxy-handler.js @@ -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}); diff --git a/proxy-blocker/manifest.json b/proxy-blocker/manifest.json index 87bed98..da36852 100644 --- a/proxy-blocker/manifest.json +++ b/proxy-blocker/manifest.json @@ -12,7 +12,7 @@ "applications": { "gecko": { - "strict_min_version": "55.0a1" + "strict_min_version": "56.0a1" } }, diff --git a/proxy-blocker/proxy/proxy-script.js b/proxy-blocker/proxy/proxy-script.js index 135295c..4a133fb 100644 --- a/proxy-blocker/proxy/proxy-script.js +++ b/proxy-blocker/proxy/proxy-script.js @@ -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 diff --git a/quicknote/popup/quicknote.js b/quicknote/popup/quicknote.js index b94c29e..04d5a81 100644 --- a/quicknote/popup/quicknote.js +++ b/quicknote/popup/quicknote.js @@ -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); diff --git a/react-es6-popup/.eslintrc.json b/react-es6-popup/.eslintrc.json new file mode 100644 index 0000000..bc30d96 --- /dev/null +++ b/react-es6-popup/.eslintrc.json @@ -0,0 +1,15 @@ +{ + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + } + }, + "env": { + "browser": true, + "es6": true, + "amd": true, + "webextensions": true + } +} diff --git a/react-es6-popup/src/popup.js b/react-es6-popup/src/popup.js index 045a618..52da0ca 100755 --- a/react-es6-popup/src/popup.js +++ b/react-es6-popup/src/popup.js @@ -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]}); }); } diff --git a/selection-to-clipboard/content-script.js b/selection-to-clipboard/content-script.js index 8fb2598..1b78c5b 100644 --- a/selection-to-clipboard/content-script.js +++ b/selection-to-clipboard/content-script.js @@ -1,7 +1,7 @@ /* copy the selected text to clipboard */ -function copySelection(e) { +function copySelection() { var selectedText = window.getSelection().toString().trim(); if (selectedText) { diff --git a/store-collected-images/README.md b/store-collected-images/README.md new file mode 100644 index 0000000..1733902 --- /dev/null +++ b/store-collected-images/README.md @@ -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. + +[![entension demo screencast](screenshots/screenshot.png "extension demo screencast")](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/). diff --git a/store-collected-images/screenshots/screenshot.png b/store-collected-images/screenshots/screenshot.png new file mode 100644 index 0000000..01341b8 Binary files /dev/null and b/store-collected-images/screenshots/screenshot.png differ diff --git a/store-collected-images/webextension-plain/.eslintignore b/store-collected-images/webextension-plain/.eslintignore new file mode 100644 index 0000000..1aa6280 --- /dev/null +++ b/store-collected-images/webextension-plain/.eslintignore @@ -0,0 +1 @@ +deps \ No newline at end of file diff --git a/store-collected-images/webextension-plain/.eslintrc b/store-collected-images/webextension-plain/.eslintrc new file mode 100644 index 0000000..f0310c8 --- /dev/null +++ b/store-collected-images/webextension-plain/.eslintrc @@ -0,0 +1,3 @@ +{ + "parser": "babel-eslint" +} \ No newline at end of file diff --git a/store-collected-images/webextension-plain/README.md b/store-collected-images/webextension-plain/README.md new file mode 100644 index 0000000..a6e2ce5 --- /dev/null +++ b/store-collected-images/webextension-plain/README.md @@ -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 ( +
    +

    A title

    +

    A text paragraph

    +
    + ); + } +} +``` + +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"), + ]); + } +} +``` diff --git a/store-collected-images/webextension-plain/background.js b/store-collected-images/webextension-plain/background.js new file mode 100644 index 0000000..436131f --- /dev/null +++ b/store-collected-images/webextension-plain/background.js @@ -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); + } +}); diff --git a/store-collected-images/webextension-plain/deps/idb-file-storage.js b/store-collected-images/webextension-plain/deps/idb-file-storage.js new file mode 100644 index 0000000..984a7f8 --- /dev/null +++ b/store-collected-images/webextension-plain/deps/idb-file-storage.js @@ -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} + * 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} + * 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} + * 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} + * 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} + * 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} + * 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} + * 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} + * 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} + * 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} + * 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} + * 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 diff --git a/store-collected-images/webextension-plain/deps/idb-file-storage.js.map b/store-collected-images/webextension-plain/deps/idb-file-storage.js.map new file mode 100644 index 0000000..72cc8f0 --- /dev/null +++ b/store-collected-images/webextension-plain/deps/idb-file-storage.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["idb-file-storage.js"],"names":["waitForDOMRequest","getFileStorage","req","onsuccess","Promise","resolve","reject","result","onerror","error","IDBPromisedFileHandle","constructor","file","lockedFile","writeQueue","closed","undefined","aborted","ensureLocked","invalidMode","Error","mode","active","reopenFileHandle","close","flush","abort","getMetadata","readAsText","size","location","readAsArrayBuffer","truncate","append","data","write","queuedWrite","nextWriteRequest","lastLocation","then","waitForQueuedWrites","IDBPromisedMutableFile","filesStorage","idb","fileName","fileType","mutableFile","fileHandle","open","getFile","persistAsFileSnapshot","snapshotName","initializedDB","runFileRequestGenerator","objectStore","getObjectStoreTransaction","put","bind","get","persist","generatorFunction","name","fileRequestsIter","processFileRequestIter","prevRequestResult","nextFileRequest","next","done","value","window","DOMRequest","IDBRequest","throw","request","IDBFileStorage","persistent","indexedDBName","objectStorageName","initializedPromise","version","IDBMutableFile","storage","dbReq","indexedDB","onupgradeneeded","db","objectStoreNames","contains","createObjectStore","transaction","createMutableFile","File","Blob","remove","delete","list","options","allKeys","getAllKeys","filteredKeys","filter","key","match","startsWith","endsWith","includes","filterFn","count","length","type","clear"],"mappings":"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","file":"idb-file-storage.js","sourcesContent":["\"use strict\";\n\n/**\n * @typedef {Object} IDBPromisedFileHandle.Metadata\n * @property {number} size\n * The size of the file in bytes.\n * @property {Date} last Modified\n * The time and date of the last change to the file.\n */\n\n/**\n * @typedef {Object} IDBFileStorage.ListFilteringOptions\n * @property {string} startsWith\n * A string to be checked with `fileNameString.startsWith(...)`.\n * @property {string} endsWith\n * A string to be checked with `fileNameString.endsWith(...)`.\n * @property {string} includes\n * A string to be checked with `fileNameString.includes(...)`.\n * @property {function} filterFn\n * A function to be used to check the file name (`filterFn(fileNameString)`).\n */\n\n/**\n * Wraps a DOMRequest into a promise, optionally transforming the result using the onsuccess\n * callback.\n *\n * @param {IDBRequest|DOMRequest} req\n * The DOMRequest instance to wrap in a Promise.\n * @param {function} [onsuccess]\n * An optional onsuccess callback which can transform the result before resolving it.\n *\n * @returns {Promise}\n * The promise which wraps the request result, rejected if the request.onerror has been\n * called.\n */\nexport function waitForDOMRequest(req, onsuccess) {\n return new Promise((resolve, reject) => {\n req.onsuccess = onsuccess ?\n (() => resolve(onsuccess(req.result))) : (() => resolve(req.result));\n req.onerror = () => reject(req.error);\n });\n}\n\n/**\n * Wraps an IDBMutableFile's FileHandle with a nicer Promise-based API.\n *\n * Instances of this class are created from the\n * {@link IDBPromisedMutableFile.open} method.\n */\nexport class IDBPromisedFileHandle {\n /**\n * @private private helper method used internally.\n */\n constructor({file, lockedFile}) {\n // All the following properties are private and it should not be needed\n // while using the API.\n\n /** @private */\n this.file = file;\n /** @private */\n this.lockedFile = lockedFile;\n /** @private */\n this.writeQueue = Promise.resolve();\n /** @private */\n this.closed = undefined;\n /** @private */\n this.aborted = undefined;\n }\n\n /**\n * @private private helper method used internally.\n */\n ensureLocked({invalidMode} = {}) {\n if (this.closed) {\n throw new Error(\"FileHandle has been closed\");\n }\n\n if (this.aborted) {\n throw new Error(\"FileHandle has been aborted\");\n }\n\n if (!this.lockedFile) {\n throw new Error(\"Invalid FileHandled\");\n }\n\n if (invalidMode && this.lockedFile.mode === invalidMode) {\n throw new Error(`FileHandle should not be opened as '${this.lockedFile.mode}'`);\n }\n if (!this.lockedFile.active) {\n // Automatically relock the file with the last open mode\n this.file.reopenFileHandle(this);\n }\n }\n\n // Promise-based MutableFile API\n\n /**\n * Provide access to the mode that has been used to open the {@link IDBPromisedMutableFile}.\n *\n * @type {\"readonly\"|\"readwrite\"|\"writeonly\"}\n */\n get mode() {\n return this.lockedFile.mode;\n }\n\n /**\n * A boolean property that is true if the lock is still active.\n *\n * @type {boolean}\n */\n get active() {\n return this.lockedFile ? this.lockedFile.active : false;\n }\n\n /**\n * Close the locked file (and wait for any written data to be flushed if needed).\n *\n * @returns {Promise}\n * A promise which is resolved when the close request has been completed\n */\n async close() {\n if (!this.lockedFile) {\n throw new Error(\"FileHandle is not open\");\n }\n\n // Wait the queued write to complete.\n await this.writeQueue;\n\n // Wait for flush request to complete if needed.\n if (this.lockedFile.active && this.lockedFile.mode !== \"readonly\") {\n await waitForDOMRequest(this.lockedFile.flush());\n }\n\n this.closed = true;\n this.lockedFile = null;\n this.writeQueue = Promise.resolve();\n }\n\n /**\n * Abort any pending data request and set the instance as aborted.\n *\n * @returns {Promise}\n * A promise which is resolved when the abort request has been completed\n */\n async abort() {\n if (this.lockedFile.active) {\n // NOTE: in the docs abort is reported to return a DOMRequest, but it doesn't seem\n // to be the case. (https://developer.mozilla.org/en-US/docs/Web/API/LockedFile/abort)\n this.lockedFile.abort();\n }\n\n this.aborted = true;\n this.lockedFile = null;\n this.writeQueue = Promise.resolve();\n }\n\n /**\n * Get the file metadata (take a look to {@link IDBPromisedFileHandle.Metadata} for more info).\n *\n * @returns {Promise<{size: number, lastModified: Date}>}\n * A promise which is resolved when the request has been completed\n */\n async getMetadata() {\n this.ensureLocked();\n return waitForDOMRequest(this.lockedFile.getMetadata());\n }\n\n /**\n * Read a given amount of data from the file as Text (optionally starting from the specified\n * location).\n *\n * @param {number} size\n * The amount of data to read.\n * @param {number} [location]\n * The location where the request should start to read the data.\n *\n * @returns {Promise}\n * A promise which resolves to the data read, when the request has been completed.\n */\n async readAsText(size, location) {\n this.ensureLocked({invalidMode: \"writeonly\"});\n if (typeof location === \"number\") {\n this.lockedFile.location = location;\n }\n return waitForDOMRequest(this.lockedFile.readAsText(size));\n }\n\n /**\n * Read a given amount of data from the file as an ArrayBufer (optionally starting from the specified\n * location).\n *\n * @param {number} size\n * The amount of data to read.\n * @param {number} [location]\n * The location where the request should start to read the data.\n *\n * @returns {Promise}\n * A promise which resolves to the data read, when the request has been completed.\n */\n async readAsArrayBuffer(size, location) {\n this.ensureLocked({invalidMode: \"writeonly\"});\n if (typeof location === \"number\") {\n this.lockedFile.location = location;\n }\n return waitForDOMRequest(this.lockedFile.readAsArrayBuffer(size));\n }\n\n /**\n * Truncate the file (optionally at a specified location).\n *\n * @param {number} [location=0]\n * The location where the file should be truncated.\n *\n * @returns {Promise}\n * A promise which is resolved once the request has been completed.\n */\n async truncate(location = 0) {\n this.ensureLocked({invalidMode: \"readonly\"});\n return waitForDOMRequest(this.lockedFile.truncate(location));\n }\n\n /**\n * Append the passed data to the end of the file.\n *\n * @param {string|ArrayBuffer} data\n * The data to append to the end of the file.\n *\n * @returns {Promise}\n * A promise which is resolved once the request has been completed.\n */\n async append(data) {\n this.ensureLocked({invalidMode: \"readonly\"});\n return waitForDOMRequest(this.lockedFile.append(data));\n }\n\n /**\n * Write data into the file (optionally starting from a defined location in the file).\n *\n * @param {string|ArrayBuffer} data\n * The data to write into the file.\n * @param {number} location\n * The location where the data should be written.\n *\n * @returns {Promise}\n * A promise which is resolved to the location where the written data ends.\n */\n async write(data, location) {\n this.ensureLocked({invalidMode: \"readonly\"});\n if (typeof location === \"number\") {\n this.lockedFile.location = location;\n }\n return waitForDOMRequest(\n this.lockedFile.write(data),\n // Resolves to the new location.\n () => {\n return this.lockedFile.location;\n }\n );\n }\n\n /**\n * Queue data to be written into the file (optionally starting from a defined location in the file).\n *\n * @param {string|ArrayBuffer} data\n * The data to write into the file.\n * @param {number} location\n * The location where the data should be written (when not specified the end of the previous\n * queued write is used).\n *\n * @returns {Promise}\n * A promise which is resolved once the request has been completed with the location where the\n * file was after the data has been writted.\n */\n queuedWrite(data, location) {\n const nextWriteRequest = async lastLocation => {\n this.ensureLocked({invalidMode: \"readonly\"});\n\n if (typeof location === \"number\") {\n return this.write(data, location);\n }\n return this.write(data, lastLocation);\n };\n\n this.writeQueue = this.writeQueue.then(nextWriteRequest);\n return this.writeQueue;\n }\n\n /**\n * Wait that any queued data has been written.\n *\n * @returns {Promise}\n * A promise which is resolved once the request has been completed with the location where the\n * file was after the data has been writted.\n */\n async waitForQueuedWrites() {\n await this.writeQueue;\n }\n}\n\n/**\n * Wraps an IDBMutableFile with a nicer Promise-based API.\n *\n * Instances of this class are created from the\n * {@link IDBFileStorage.createMutableFile} method.\n */\nexport class IDBPromisedMutableFile {\n /**\n * @private private helper method used internally.\n */\n constructor({filesStorage, idb, fileName, fileType, mutableFile}) {\n // All the following properties are private and it should not be needed\n // while using the API.\n\n /** @private */\n this.filesStorage = filesStorage;\n /** @private */\n this.idb = idb;\n /** @private */\n this.fileName = fileName;\n /** @private */\n this.fileType = fileType;\n /** @private */\n this.mutableFile = mutableFile;\n }\n\n /**\n * @private private helper method used internally.\n */\n reopenFileHandle(fileHandle) {\n fileHandle.lockedFile = this.mutableFile.open(fileHandle.mode);\n }\n\n // API methods.\n\n /**\n * Open a mutable file for reading/writing data.\n *\n * @param {\"readonly\"|\"readwrite\"|\"writeonly\"} mode\n * The mode of the created IDBPromisedFileHandle instance.\n *\n * @returns {IDBPromisedFileHandle}\n * The created IDBPromisedFileHandle instance.\n */\n open(mode) {\n if (this.lockedFile) {\n throw new Error(\"MutableFile cannot be opened twice\");\n }\n const lockedFile = this.mutableFile.open(mode);\n\n return new IDBPromisedFileHandle({file: this, lockedFile});\n }\n\n /**\n * Get a {@link File} instance of this mutable file.\n *\n * @returns {Promise}\n * A promise resolved to the File instance.\n *\n * To read the actual content of the mutable file as a File object,\n * it is often better to use {@link IDBPromisedMutableFile.saveAsFileSnapshot}\n * to save a persistent snapshot of the file in the IndexedDB store,\n * or reading it directly using the {@link IDBPromisedFileHandle} instance\n * returned by the {@link IDBPromisedMutableFile.open} method.\n *\n * The reason is that to be able to read the content of the returned file\n * a lockfile have be keep the file open, e.d. as in the following example.\n *\n * @example\n * ...\n * let waitSnapshotStored;\n * await mutableFile.runFileRequestGenerator(function* (lockedFile) {\n * const file = yield lockedFile.mutableFile.getFile();\n * // read the file content or turn it into a persistent object of its own\n * // (e.g. by saving it back into IndexedDB as its snapshot in form of a File object,\n * // or converted into a data url, a string or an array buffer)\n *\n * waitSnapshotStored = tmpFiles.put(\"${filename}/last_snapshot\", file);\n * }\n *\n * await waitSnapshotStored;\n * let fileSnapshot = await tmpFiles.get(\"${filename}/last_snapshot\");\n * ...\n * // now you can use fileSnapshot even if the mutableFile lock is not active anymore.\n */\n getFile() {\n return waitForDOMRequest(this.mutableFile.getFile());\n }\n\n /**\n * Persist the content of the mutable file into the files storage\n * as a File, using the specified snapshot name and return the persisted File instance.\n *\n * @returns {Promise}\n * A promise resolved to the File instance.\n *\n * @example\n *\n * const file = await mutableFile.persistAsFileSnapshot(`${filename}/last_snapshot`);\n * const blobURL = URL.createObjectURL(file);\n * ...\n * // The blob URL is still valid even if the mutableFile is not active anymore.\n */\n async persistAsFileSnapshot(snapshotName) {\n if (snapshotName === this.fileName) {\n throw new Error(\"Snapshot name and the file name should be different\");\n }\n\n const idb = await this.filesStorage.initializedDB();\n await this.runFileRequestGenerator(function* () {\n const file = yield this.mutableFile.getFile();\n const objectStore = this.filesStorage.getObjectStoreTransaction({idb, mode: \"readwrite\"});\n\n yield objectStore.put(file, snapshotName);\n }.bind(this));\n\n return this.filesStorage.get(snapshotName);\n }\n\n /**\n * Persist the this mutable file into its related IDBFileStorage.\n *\n * @returns {Promise}\n * A promise resolved on the mutable file has been persisted into IndexedDB.\n */\n persist() {\n return this.filesStorage.put(this.fileName, this);\n }\n\n /**\n * Run a generator function which can run a sequence of FileRequests\n * without the lockfile to become inactive.\n *\n * This method should be rarely needed, mostly to optimize a sequence of\n * file operations without the file to be closed and automatically re-opened\n * between two file requests.\n *\n * @param {function* (lockedFile) {...}} generatorFunction\n * @param {\"readonly\"|\"readwrite\"|\"writeonly\"} mode\n *\n * @example\n * (async function () {\n * const tmpFiles = await IDBFiles.getFileStorage({name: \"tmpFiles\"});\n * const mutableFile = await tmpFiles.createMutableFile(\"test-mutable-file.txt\");\n *\n * let allFileData;\n *\n * function* fileOperations(lockedFile) {\n * yield lockedFile.write(\"some data\");\n * yield lockedFile.write(\"more data\");\n * const metadata = yield lockedFile.getMetadata();\n *\n * lockedFile.location = 0;\n * allFileData = yield lockedFile.readAsText(metadata.size);\n * }\n *\n * await mutableFile.runFileRequestGenerator(fileOperations, \"readwrite\");\n *\n * console.log(\"File Data\", allFileData);\n * })();\n */\n async runFileRequestGenerator(generatorFunction, mode) {\n if (generatorFunction.constructor.name !== \"GeneratorFunction\") {\n throw new Error(\"runGenerator parameter should be a generator function\");\n }\n\n await new Promise((resolve, reject) => {\n const lockedFile = this.mutableFile.open(mode || \"readwrite\");\n const fileRequestsIter = generatorFunction(lockedFile);\n\n const processFileRequestIter = prevRequestResult => {\n const nextFileRequest = fileRequestsIter.next(prevRequestResult);\n if (nextFileRequest.done) {\n resolve();\n return;\n } else if (!(nextFileRequest.value instanceof window.DOMRequest ||\n nextFileRequest.value instanceof window.IDBRequest)) {\n const error = new Error(\"FileRequestGenerator should only yield DOMRequest instances\");\n fileRequestsIter.throw(error);\n reject(error);\n return;\n }\n\n const request = nextFileRequest.value;\n if (request.onsuccess || request.onerror) {\n const error = new Error(\"DOMRequest onsuccess/onerror callbacks are already set\");\n fileRequestsIter.throw(error);\n reject(error);\n } else {\n request.onsuccess = () => processFileRequestIter(request.result);\n request.onerror = () => reject(request.error);\n }\n };\n\n processFileRequestIter();\n });\n }\n}\n\n/**\n * Provides a Promise-based API to store files into an IndexedDB.\n *\n * Instances of this class are created using the exported\n * {@link getFileStorage} function.\n */\nexport class IDBFileStorage {\n /**\n * @private private helper method used internally.\n */\n constructor({name, persistent} = {}) {\n // All the following properties are private and it should not be needed\n // while using the API.\n\n /** @private */\n this.name = name;\n /** @private */\n this.persistent = persistent;\n /** @private */\n this.indexedDBName = `IDBFilesStorage-DB-${this.name}`;\n /** @private */\n this.objectStorageName = \"IDBFilesObjectStorage\";\n /** @private */\n this.initializedPromise = undefined;\n\n // TODO: evalutate schema migration between library versions?\n /** @private */\n this.version = 1.0;\n }\n\n /**\n * @private private helper method used internally.\n */\n initializedDB() {\n if (this.initializedPromise) {\n return this.initializedPromise;\n }\n\n this.initializedPromise = (async () => {\n if (window.IDBMutableFile && this.persistent) {\n this.version = {version: this.version, storage: \"persistent\"};\n }\n const dbReq = indexedDB.open(this.indexedDBName, this.version);\n\n dbReq.onupgradeneeded = () => {\n const db = dbReq.result;\n if (!db.objectStoreNames.contains(this.objectStorageName)) {\n db.createObjectStore(this.objectStorageName);\n }\n };\n\n return waitForDOMRequest(dbReq);\n })();\n\n return this.initializedPromise;\n }\n\n /**\n * @private private helper method used internally.\n */\n getObjectStoreTransaction({idb, mode} = {}) {\n const transaction = idb.transaction([this.objectStorageName], mode);\n return transaction.objectStore(this.objectStorageName);\n }\n\n /**\n * Create a new IDBPromisedMutableFile instance (where the IDBMutableFile is supported)\n *\n * @param {string} fileName\n * The fileName associated to the new IDBPromisedMutableFile instance.\n * @param {string} [fileType=\"text\"]\n * The mime type associated to the file.\n *\n * @returns {IDBPromisedMutableFile}\n * The newly created {@link IDBPromisedMutableFile} instance.\n */\n async createMutableFile(fileName, fileType = \"text\") {\n if (!window.IDBMutableFile) {\n throw new Error(\"This environment does not support IDBMutableFile\");\n }\n const idb = await this.initializedDB();\n const mutableFile = await waitForDOMRequest(\n idb.createMutableFile(fileName, fileType)\n );\n return new IDBPromisedMutableFile({\n filesStorage: this, idb, fileName, fileType, mutableFile\n });\n }\n\n /**\n * Put a file object into the IDBFileStorage, it overwrites an existent file saved with the\n * fileName if any.\n *\n * @param {string} fileName\n * The key associated to the file in the IDBFileStorage.\n * @param {Blob|File|IDBPromisedMutableFile|IDBMutableFile} file\n * The file to be persisted.\n *\n * @returns {Promise}\n * A promise resolved when the request has been completed.\n */\n async put(fileName, file) {\n if (!fileName || typeof fileName !== \"string\") {\n throw new Error(\"fileName parameter is mandatory\");\n }\n\n if (!(file instanceof File) && !(file instanceof Blob) &&\n !(window.IDBMutableFile && file instanceof window.IDBMutableFile) &&\n !(file instanceof IDBPromisedMutableFile)) {\n throw new Error(`Unable to persist ${fileName}. Unknown file type.`);\n }\n\n if (file instanceof IDBPromisedMutableFile) {\n file = file.mutableFile;\n }\n\n const idb = await this.initializedDB();\n const objectStore = this.getObjectStoreTransaction({idb, mode: \"readwrite\"});\n return waitForDOMRequest(objectStore.put(file, fileName));\n }\n\n /**\n * Remove a file object from the IDBFileStorage.\n *\n * @param {string} fileName\n * The fileName (the associated IndexedDB key) to remove from the IDBFileStorage.\n *\n * @returns {Promise}\n * A promise resolved when the request has been completed.\n */\n async remove(fileName) {\n if (!fileName) {\n throw new Error(\"fileName parameter is mandatory\");\n }\n\n const idb = await this.initializedDB();\n const objectStore = this.getObjectStoreTransaction({idb, mode: \"readwrite\"});\n return waitForDOMRequest(objectStore.delete(fileName));\n }\n\n /**\n * List the names of the files stored in the IDBFileStorage.\n *\n * (If any filtering options has been specified, only the file names that match\n * all the filters are included in the result).\n *\n * @param {IDBFileStorage.ListFilteringOptions} options\n * The optional filters to apply while listing the stored file names.\n *\n * @returns {Promise}\n * A promise resolved to the array of the filenames that has been found.\n */\n async list(options) {\n const idb = await this.initializedDB();\n const objectStore = this.getObjectStoreTransaction({idb});\n const allKeys = await waitForDOMRequest(objectStore.getAllKeys());\n\n let filteredKeys = allKeys;\n\n if (options) {\n filteredKeys = filteredKeys.filter(key => {\n let match = true;\n\n if (typeof options.startsWith === \"string\") {\n match = match && key.startsWith(options.startsWith);\n }\n\n if (typeof options.endsWith === \"string\") {\n match = match && key.endsWith(options.endsWith);\n }\n\n if (typeof options.includes === \"string\") {\n match = match && key.includes(options.includes);\n }\n\n if (typeof options.filterFn === \"function\") {\n match = match && options.filterFn(key);\n }\n\n return match;\n });\n }\n\n return filteredKeys;\n }\n\n /**\n * Count the number of files stored in the IDBFileStorage.\n *\n * (If any filtering options has been specified, only the file names that match\n * all the filters are included in the final count).\n *\n * @param {IDBFileStorage.ListFilteringOptions} options\n * The optional filters to apply while listing the stored file names.\n *\n * @returns {Promise}\n * A promise resolved to the number of files that has been found.\n */\n async count(options) {\n if (!options) {\n const idb = await this.initializedDB();\n const objectStore = this.getObjectStoreTransaction({idb});\n return waitForDOMRequest(objectStore.count());\n }\n\n const filteredKeys = await this.list(options);\n return filteredKeys.length;\n }\n\n /**\n * Retrieve a file stored in the IDBFileStorage by key.\n *\n * @param {string} fileName\n * The key to use to retrieve the file from the IDBFileStorage.\n *\n * @returns {Promise}\n * A promise resolved once the file stored in the IDBFileStorage has been retrieved.\n */\n async get(fileName) {\n const idb = await this.initializedDB();\n const objectStore = this.getObjectStoreTransaction({idb});\n return waitForDOMRequest(objectStore.get(fileName)).then(result => {\n if (window.IDBMutableFile && result instanceof window.IDBMutableFile) {\n return new IDBPromisedMutableFile({\n filesStorage: this,\n idb,\n fileName,\n fileType: result.type,\n mutableFile: result\n });\n }\n\n return result;\n });\n }\n\n /**\n * Remove all the file objects stored in the IDBFileStorage.\n *\n * @returns {Promise}\n * A promise resolved once the IDBFileStorage has been cleared.\n */\n async clear() {\n const idb = await this.initializedDB();\n const objectStore = this.getObjectStoreTransaction({idb, mode: \"readwrite\"});\n return waitForDOMRequest(objectStore.clear());\n }\n}\n\n/**\n * Retrieve an IDBFileStorage instance by name (and it creates the indexedDB if it doesn't\n * exist yet).\n *\n * @param {Object} [param]\n * @param {string} [param.name=\"default\"]\n * The name associated to the IDB File Storage.\n * @param {boolean} [param.persistent]\n * Optionally enable persistent storage mode (not enabled by default).\n *\n * @returns {IDBFileStorage}\n * The IDBFileStorage instance with the given name.\n */\nexport async function getFileStorage({name, persistent} = {}) {\n const filesStorage = new IDBFileStorage({name: name || \"default\", persistent});\n await filesStorage.initializedDB();\n return filesStorage;\n}\n\n/**\n * @external {Blob} https://developer.mozilla.org/en-US/docs/Web/API/Blob\n */\n\n/**\n * @external {DOMRequest} https://developer.mozilla.org/en/docs/Web/API/DOMRequest\n */\n\n/**\n * @external {File} https://developer.mozilla.org/en-US/docs/Web/API/File\n */\n\n/**\n * @external {IDBMutableFile} https://developer.mozilla.org/en-US/docs/Web/API/IDBMutableFile\n */\n\n/**\n * @external {IDBRequest} https://developer.mozilla.org/en-US/docs/Web/API/IDBRequest\n */\n"]} \ No newline at end of file diff --git a/store-collected-images/webextension-plain/deps/uuidv4.js b/store-collected-images/webextension-plain/deps/uuidv4.js new file mode 100644 index 0000000..76d869b --- /dev/null +++ b/store-collected-images/webextension-plain/deps/uuidv4.js @@ -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>>((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)}); \ No newline at end of file diff --git a/store-collected-images/webextension-plain/images/icon.png b/store-collected-images/webextension-plain/images/icon.png new file mode 100644 index 0000000..81fede1 Binary files /dev/null and b/store-collected-images/webextension-plain/images/icon.png differ diff --git a/store-collected-images/webextension-plain/images/icon16.png b/store-collected-images/webextension-plain/images/icon16.png new file mode 100644 index 0000000..8d4b5cc Binary files /dev/null and b/store-collected-images/webextension-plain/images/icon16.png differ diff --git a/store-collected-images/webextension-plain/manifest.json b/store-collected-images/webextension-plain/manifest.json new file mode 100755 index 0000000..1b9cee0 --- /dev/null +++ b/store-collected-images/webextension-plain/manifest.json @@ -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", + "" + ] +} diff --git a/store-collected-images/webextension-plain/navigate-collection.css b/store-collected-images/webextension-plain/navigate-collection.css new file mode 100755 index 0000000..0919083 --- /dev/null +++ b/store-collected-images/webextension-plain/navigate-collection.css @@ -0,0 +1 @@ +@import "shared.css"; \ No newline at end of file diff --git a/store-collected-images/webextension-plain/navigate-collection.html b/store-collected-images/webextension-plain/navigate-collection.html new file mode 100755 index 0000000..06b0889 --- /dev/null +++ b/store-collected-images/webextension-plain/navigate-collection.html @@ -0,0 +1,21 @@ + + + + + + + +
    +

    Stored images

    + + + +
      +
      + + + + + + + diff --git a/store-collected-images/webextension-plain/navigate-collection.js b/store-collected-images/webextension-plain/navigate-collection.js new file mode 100644 index 0000000..b5d49f9 --- /dev/null +++ b/store-collected-images/webextension-plain/navigate-collection.js @@ -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')); diff --git a/store-collected-images/webextension-plain/popup.css b/store-collected-images/webextension-plain/popup.css new file mode 100755 index 0000000..2c7ea8d --- /dev/null +++ b/store-collected-images/webextension-plain/popup.css @@ -0,0 +1,12 @@ +@import "shared.css"; + +html, body { + width: 250px; + margin: 0; + padding: 0; + margin-left: 1em; +} + +input { + width: 90%; +} diff --git a/store-collected-images/webextension-plain/popup.html b/store-collected-images/webextension-plain/popup.html new file mode 100755 index 0000000..d7b4798 --- /dev/null +++ b/store-collected-images/webextension-plain/popup.html @@ -0,0 +1,22 @@ + + + + + + + +
      +

      Collected images

      +

      + + +
        +
          +
      + + + + + + + diff --git a/store-collected-images/webextension-plain/popup.js b/store-collected-images/webextension-plain/popup.js new file mode 100644 index 0000000..0b3fc6a --- /dev/null +++ b/store-collected-images/webextension-plain/popup.js @@ -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}); + } +}); diff --git a/store-collected-images/webextension-plain/shared.css b/store-collected-images/webextension-plain/shared.css new file mode 100644 index 0000000..e31f58c --- /dev/null +++ b/store-collected-images/webextension-plain/shared.css @@ -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); +} \ No newline at end of file diff --git a/store-collected-images/webextension-plain/utils/handle-window-drag-and-drop.js b/store-collected-images/webextension-plain/utils/handle-window-drag-and-drop.js new file mode 100644 index 0000000..34b74ef --- /dev/null +++ b/store-collected-images/webextension-plain/utils/handle-window-drag-and-drop.js @@ -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; +} diff --git a/store-collected-images/webextension-plain/utils/image-store.js b/store-collected-images/webextension-plain/utils/image-store.js new file mode 100644 index 0000000..2c52716 --- /dev/null +++ b/store-collected-images/webextension-plain/utils/image-store.js @@ -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); + } +} diff --git a/store-collected-images/webextension-with-webpack/.eslintrc b/store-collected-images/webextension-with-webpack/.eslintrc new file mode 100644 index 0000000..1adf976 --- /dev/null +++ b/store-collected-images/webextension-with-webpack/.eslintrc @@ -0,0 +1,6 @@ +{ + "parser": "babel-eslint", + "env": { + "commonjs": true + } +} \ No newline at end of file diff --git a/store-collected-images/webextension-with-webpack/.gitignore b/store-collected-images/webextension-with-webpack/.gitignore new file mode 100644 index 0000000..20ccaa9 --- /dev/null +++ b/store-collected-images/webextension-with-webpack/.gitignore @@ -0,0 +1,5 @@ +# Ignore build artifacts and other files. +.DS_Store +yarn.lock +extension/dist +node_modules diff --git a/store-collected-images/webextension-with-webpack/README.md b/store-collected-images/webextension-with-webpack/README.md new file mode 100644 index 0000000..b768140 --- /dev/null +++ b/store-collected-images/webextension-with-webpack/README.md @@ -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. diff --git a/store-collected-images/webextension-with-webpack/extension/images/icon.png b/store-collected-images/webextension-with-webpack/extension/images/icon.png new file mode 100644 index 0000000..81fede1 Binary files /dev/null and b/store-collected-images/webextension-with-webpack/extension/images/icon.png differ diff --git a/store-collected-images/webextension-with-webpack/extension/images/icon16.png b/store-collected-images/webextension-with-webpack/extension/images/icon16.png new file mode 100644 index 0000000..8d4b5cc Binary files /dev/null and b/store-collected-images/webextension-with-webpack/extension/images/icon16.png differ diff --git a/store-collected-images/webextension-with-webpack/extension/manifest.json b/store-collected-images/webextension-with-webpack/extension/manifest.json new file mode 100755 index 0000000..f99de3c --- /dev/null +++ b/store-collected-images/webextension-with-webpack/extension/manifest.json @@ -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", + "" + ] +} diff --git a/store-collected-images/webextension-with-webpack/extension/navigate-collection.css b/store-collected-images/webextension-with-webpack/extension/navigate-collection.css new file mode 100755 index 0000000..0919083 --- /dev/null +++ b/store-collected-images/webextension-with-webpack/extension/navigate-collection.css @@ -0,0 +1 @@ +@import "shared.css"; \ No newline at end of file diff --git a/store-collected-images/webextension-with-webpack/extension/navigate-collection.html b/store-collected-images/webextension-with-webpack/extension/navigate-collection.html new file mode 100755 index 0000000..98fadb7 --- /dev/null +++ b/store-collected-images/webextension-with-webpack/extension/navigate-collection.html @@ -0,0 +1,11 @@ + + + + + + + +
      + + + diff --git a/store-collected-images/webextension-with-webpack/extension/popup.css b/store-collected-images/webextension-with-webpack/extension/popup.css new file mode 100755 index 0000000..2c7ea8d --- /dev/null +++ b/store-collected-images/webextension-with-webpack/extension/popup.css @@ -0,0 +1,12 @@ +@import "shared.css"; + +html, body { + width: 250px; + margin: 0; + padding: 0; + margin-left: 1em; +} + +input { + width: 90%; +} diff --git a/store-collected-images/webextension-with-webpack/extension/popup.html b/store-collected-images/webextension-with-webpack/extension/popup.html new file mode 100755 index 0000000..a005aa0 --- /dev/null +++ b/store-collected-images/webextension-with-webpack/extension/popup.html @@ -0,0 +1,11 @@ + + + + + + + +
      + + + diff --git a/store-collected-images/webextension-with-webpack/extension/shared.css b/store-collected-images/webextension-with-webpack/extension/shared.css new file mode 100644 index 0000000..e31f58c --- /dev/null +++ b/store-collected-images/webextension-with-webpack/extension/shared.css @@ -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); +} \ No newline at end of file diff --git a/store-collected-images/webextension-with-webpack/package.json b/store-collected-images/webextension-with-webpack/package.json new file mode 100644 index 0000000..ce6ec9c --- /dev/null +++ b/store-collected-images/webextension-with-webpack/package.json @@ -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" + ] + } +} diff --git a/store-collected-images/webextension-with-webpack/src/background.js b/store-collected-images/webextension-with-webpack/src/background.js new file mode 100644 index 0000000..436131f --- /dev/null +++ b/store-collected-images/webextension-with-webpack/src/background.js @@ -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); + } +}); diff --git a/store-collected-images/webextension-with-webpack/src/navigate-collection.js b/store-collected-images/webextension-with-webpack/src/navigate-collection.js new file mode 100644 index 0000000..79ba2ad --- /dev/null +++ b/store-collected-images/webextension-with-webpack/src/navigate-collection.js @@ -0,0 +1,66 @@ +"use strict"; + +import React from 'react'; +import ReactDOM from 'react-dom'; + +import {loadStoredImages, removeStoredImages} from './utils/image-store'; + +class NavigateCollectionUI extends React.Component { + constructor(props) { + super(props); + this.state = { + storedImages: [], + }; + + this.onFilterUpdated = this.onFilterUpdated.bind(this); + this.onReload = this.onFilterUpdated; + + this.onDelete = this.onDelete.bind(this); + } + + componentDidMount() { + // Load the stored image once the component has been rendered in the page. + this.onFilterUpdated(); + } + + onFilterUpdated() { + loadStoredImages(this.refs.imageFilter.value) + .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; + + return ( +
      +

      Stored images

      + + + +
        + { + storedImages.map(({storedName, blobUrl}) => { + const onClickedImage = () => { + this.refs.imageFilter.value = storedName; + this.onFilterUpdated(); + }; + return
      • ; + }) + } +
      +
      + ); + } +} + +ReactDOM.render(, document.getElementById('app')); diff --git a/store-collected-images/webextension-with-webpack/src/popup.js b/store-collected-images/webextension-with-webpack/src/popup.js new file mode 100755 index 0000000..56d933c --- /dev/null +++ b/store-collected-images/webextension-with-webpack/src/popup.js @@ -0,0 +1,114 @@ +"use strict"; + +import React from 'react'; +import ReactDOM from 'react-dom'; +import uuidV4 from 'uuid/v4'; + +import preventWindowDragAndDrop from './utils/handle-window-drag-and-drop'; +import {saveCollectedBlobs} from './utils/image-store'; + +class Popup extends React.Component { + constructor(props) { + super(props); + this.state = { + collectedBlobs: [], + lastMessage: undefined, + }; + + this.onClick = this.onClick.bind(this); + } + + onClick() { + if (!this.refs.collectionName.value) { + this.setState({ + lastMessage: {text: "The collection name is mandatory.", type: "error"}, + }); + + // Clear the error message after a 2s timeout. + setTimeout(() => { + this.setState({lastMessage: undefined}); + }, 2000); + + return; + } + + saveCollectedBlobs(this.refs.collectionName.value, this.state.collectedBlobs) + .then(() => { + this.setState({ + lastMessage: {text: "All the collected images have been saved", type: "success"}, + collectedBlobs: [], + }); + + // Clear the error message after a 2s timeout. + setTimeout(() => { + this.setState({lastMessage: undefined}); + }, 2000); + }) + .catch((err) => { + this.setState({ + lastMessage: {text: `Failed to save collected images: ${err}`, type: "error"}, + }); + + // Clear the error message after a 2s timeout. + setTimeout(() => { + this.setState({lastMessage: undefined}); + }, 2000); + }); + } + + render() { + const {collectedBlobs, lastMessage} = this.state; + + return ( +
      +

      Collected images

      + {lastMessage &&

      {lastMessage.text}

      } + + +
        + { + collectedBlobs.map(({uuid, blobUrl}) => { + return
      • ; + }) + } +
      +
      + ); + } +} + +const popup = ReactDOM.render(, 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}); + } +}); diff --git a/store-collected-images/webextension-with-webpack/src/utils/handle-window-drag-and-drop.js b/store-collected-images/webextension-with-webpack/src/utils/handle-window-drag-and-drop.js new file mode 100644 index 0000000..5b0ef7b --- /dev/null +++ b/store-collected-images/webextension-with-webpack/src/utils/handle-window-drag-and-drop.js @@ -0,0 +1,12 @@ +"use strict"; + +function preventDefault(ev) { + ev.preventDefault(); + return true; +} + +export default function preventWindowDragAndDrop() { + window.ondragover = preventDefault; + window.ondragend = preventDefault; + window.ondrop = preventDefault; +} diff --git a/store-collected-images/webextension-with-webpack/src/utils/image-store.js b/store-collected-images/webextension-with-webpack/src/utils/image-store.js new file mode 100644 index 0000000..b9da5ea --- /dev/null +++ b/store-collected-images/webextension-with-webpack/src/utils/image-store.js @@ -0,0 +1,51 @@ +"use strict"; + +// Import the `getFileStorage` helper from the idb-file-storage npm dependency. +import {getFileStorage} from 'idb-file-storage/src/idb-file-storage'; + +export async function saveCollectedBlobs(collectionName, collectedBlobs) { + // Retrieve a named file storage (it creates a new one if it doesn't exist yet). + const storedImages = await getFileStorage({name: "stored-images"}); + + for (const item of collectedBlobs) { + // Save all the collected blobs in an IndexedDB key named based on the collectionName + // and a randomly generated uuid. + await storedImages.put(`${collectionName}/${item.uuid}`, item.blob); + } +} + +export async function loadStoredImages(filter) { + // Retrieve a named file storage (it creates a new one if it doesn't exist yet). + const imagesStore = await getFileStorage({name: "stored-images"}); + + let listOptions = filter ? {includes: filter} : undefined; + + // List the existent stored files (optionally filtered). + const imagesList = await imagesStore.list(listOptions); + + let storedImages = []; + + for (const storedName of imagesList) { + // Retrieve the stored blob by name. + const blob = await imagesStore.get(storedName); + + // convert the Blob object into a blob URL and store it into the + // array of the results returned by this function. + storedImages.push({storedName, blobUrl: URL.createObjectURL(blob)}); + } + + return storedImages; +} + +export async function removeStoredImages(storedImages) { + // Retrieve a named file storage (it creates a new one if it doesn't exist yet). + const imagesStore = await getFileStorage({name: "stored-images"}); + + for (const storedImage of storedImages) { + // Revoke the blob URL. + URL.revokeObjectURL(storedImage.blobUrl); + + // Remove the stored blob by name. + await imagesStore.remove(storedImage.storedName); + } +} diff --git a/store-collected-images/webextension-with-webpack/webpack.config.js b/store-collected-images/webextension-with-webpack/webpack.config.js new file mode 100644 index 0000000..a9c4df2 --- /dev/null +++ b/store-collected-images/webextension-with-webpack/webpack.config.js @@ -0,0 +1,52 @@ +/* eslint-env node */ + +const path = require('path'); +const webpack = require('webpack'); + +module.exports = { + entry: { + // Each entry in here would declare a file that needs to be transpiled + // and included in the extension source. + // For example, you could add a background script like: + background: 'background.js', + popup: 'popup.js', + 'navigate-collection': 'navigate-collection.js', + }, + output: { + // This copies each source entry into the extension dist folder named + // after its entry config key. + path: path.join(__dirname, 'extension', 'dist'), + filename: '[name].js', + }, + module: { + rules: [{ + exclude: ['/node_modules/', '!/node_modules/idb-file-storage'], + test: /\.js$/, + use: [ + // This transpiles all code (except for third party modules) using Babel. + { + // Babel options are in .babelrc + loader: 'babel-loader', + }, + ] + }] + }, + resolve: { + // This allows you to import modules just like you would in a NodeJS app. + extensions: ['.js', '.jsx'], + modules: [ + path.join(__dirname, 'src'), + 'node_modules', + ], + }, + plugins: [ + // Since some NodeJS modules expect to be running in Node, it is helpful + // to set this environment var to avoid reference errors. + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify('production'), + }), + ], + // This will expose source map files so that errors will point to your + // original source files instead of the transpiled files. + devtool: 'sourcemap', +}; diff --git a/tabs-tabs-tabs/tabs.html b/tabs-tabs-tabs/tabs.html index 6113ed0..81238cf 100644 --- a/tabs-tabs-tabs/tabs.html +++ b/tabs-tabs-tabs/tabs.html @@ -32,10 +32,6 @@ Reset zoom
      Zoom out
      -
      - - Highlight (only supported by Chrome) -
      diff --git a/tabs-tabs-tabs/tabs.js b/tabs-tabs-tabs/tabs.js index dba3dfc..4580520 100644 --- a/tabs-tabs-tabs/tabs.js +++ b/tabs-tabs-tabs/tabs.js @@ -47,7 +47,7 @@ function getCurrentWindowTabs() { return browser.tabs.query({currentWindow: true}); } -document.addEventListener("click", function(e) { +document.addEventListener("click", (e) => { function callOnActiveTab(callback) { getCurrentWindowTabs().then((tabs) => { for (var tab of tabs) { @@ -160,23 +160,16 @@ document.addEventListener("click", function(e) { }); }); } - // Currently (11/2/2016) only supported by Chrome - else if (e.target.id === "tabs-highlight") { // highlights current tab and next tab (cycles back to first tab if current tab is the last one) - callOnActiveTab((tab, tabs) => { - next = (tab.index+1) % tabs.length; - browser.tabs.highlight({tabs:[tab.index, next]}); - }); - } else if (e.target.classList.contains('switch-tabs')) { var tabId = +e.target.getAttribute('href'); - chrome.tabs.query({ + browser.tabs.query({ currentWindow: true - }, function(tabs) { + }).then((tabs) => { for (var tab of tabs) { if (tab.id === tabId) { - chrome.tabs.update(tabId, { + browser.tabs.update(tabId, { active: true }); } @@ -188,7 +181,7 @@ document.addEventListener("click", function(e) { }); //onRemoved listener. fired when tab is removed -browser.tabs.onRemoved.addListener(function(tabId, removeInfo){ +browser.tabs.onRemoved.addListener((tabId, removeInfo) => { console.log(`The tab with id: ${tabId}, is closing`); if(removeInfo.isWindowClosing) { @@ -199,7 +192,7 @@ browser.tabs.onRemoved.addListener(function(tabId, removeInfo){ }); //onMoved listener. fired when tab is moved into the same window -browser.tabs.onMoved.addListener(function(tabId, moveInfo){ +browser.tabs.onMoved.addListener((tabId, moveInfo) => { var startIndex = moveInfo.fromIndex; var endIndex = moveInfo.toIndex; console.log(`Tab with id: ${tabId} moved from index: ${startIndex} to index: ${endIndex}`); diff --git a/themes/animated/README.md b/themes/animated/README.md new file mode 100644 index 0000000..f1e4744 --- /dev/null +++ b/themes/animated/README.md @@ -0,0 +1,9 @@ +# Themes: Animated + +## What it does + +Employs an animated PNG image as the headerURL image in a theme. + +## What it shows + +How to use an animated image in a theme. diff --git a/themes/animated/manifest.json b/themes/animated/manifest.json new file mode 100755 index 0000000..e7c9751 --- /dev/null +++ b/themes/animated/manifest.json @@ -0,0 +1,19 @@ +{ + + "description": "Theme using an animated PNG file as the headerURL image. See https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Examples#themes", + "manifest_version": 2, + "name": "animated", + "version": "1.0", + "homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/themes/animated", + + "theme": { + "images": { + "headerURL": "parrot.png" + }, + + "colors": { + "accentcolor": "#ffffff", + "textcolor": "#000" + } + } +} diff --git a/themes/animated/parrot.png b/themes/animated/parrot.png new file mode 100755 index 0000000..e77366b Binary files /dev/null and b/themes/animated/parrot.png differ diff --git a/themes/animated/parrot_frames/1.png b/themes/animated/parrot_frames/1.png new file mode 100755 index 0000000..2acd576 Binary files /dev/null and b/themes/animated/parrot_frames/1.png differ diff --git a/themes/animated/parrot_frames/10.png b/themes/animated/parrot_frames/10.png new file mode 100755 index 0000000..071b908 Binary files /dev/null and b/themes/animated/parrot_frames/10.png differ diff --git a/themes/animated/parrot_frames/2.png b/themes/animated/parrot_frames/2.png new file mode 100755 index 0000000..cca931a Binary files /dev/null and b/themes/animated/parrot_frames/2.png differ diff --git a/themes/animated/parrot_frames/3.png b/themes/animated/parrot_frames/3.png new file mode 100755 index 0000000..7c57152 Binary files /dev/null and b/themes/animated/parrot_frames/3.png differ diff --git a/themes/animated/parrot_frames/4.png b/themes/animated/parrot_frames/4.png new file mode 100755 index 0000000..14dff5e Binary files /dev/null and b/themes/animated/parrot_frames/4.png differ diff --git a/themes/animated/parrot_frames/5.png b/themes/animated/parrot_frames/5.png new file mode 100755 index 0000000..e1ff036 Binary files /dev/null and b/themes/animated/parrot_frames/5.png differ diff --git a/themes/animated/parrot_frames/6.png b/themes/animated/parrot_frames/6.png new file mode 100755 index 0000000..17c05ec Binary files /dev/null and b/themes/animated/parrot_frames/6.png differ diff --git a/themes/animated/parrot_frames/7.png b/themes/animated/parrot_frames/7.png new file mode 100755 index 0000000..3f28616 Binary files /dev/null and b/themes/animated/parrot_frames/7.png differ diff --git a/themes/animated/parrot_frames/8.png b/themes/animated/parrot_frames/8.png new file mode 100755 index 0000000..eb02e61 Binary files /dev/null and b/themes/animated/parrot_frames/8.png differ diff --git a/themes/animated/parrot_frames/9.png b/themes/animated/parrot_frames/9.png new file mode 100755 index 0000000..f73dd3d Binary files /dev/null and b/themes/animated/parrot_frames/9.png differ diff --git a/themes/weta_fade/README.md b/themes/weta_fade/README.md new file mode 100644 index 0000000..e2fd8d1 --- /dev/null +++ b/themes/weta_fade/README.md @@ -0,0 +1,10 @@ +# Themes: weta_fade + +## What it does + +Employs a PNG image as the headerURL image in a theme. + +## What it shows + +How to create a single image theme, using a faded edge and background color to ensure +the image works well on a range of screen sizes. diff --git a/themes/weta_fade/manifest.json b/themes/weta_fade/manifest.json new file mode 100644 index 0000000..30145f7 --- /dev/null +++ b/themes/weta_fade/manifest.json @@ -0,0 +1,19 @@ +{ + + "description": "Theme using a PNG as the headerURL image, employing a faded edge. See https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Examples#themes", + "manifest_version": 2, + "name": "weta_fade", + "version": "1.0", + "homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/themes/weta_fade", + + "theme": { + "images": { + "headerURL": "weta.png" + }, + + "colors": { + "accentcolor": "#adb09f", + "textcolor": "#000" + } + } +} diff --git a/themes/weta_fade/weta.png b/themes/weta_fade/weta.png new file mode 100755 index 0000000..8b79595 Binary files /dev/null and b/themes/weta_fade/weta.png differ diff --git a/themes/weta_fade_chrome/README.md b/themes/weta_fade_chrome/README.md new file mode 100644 index 0000000..2de7d43 --- /dev/null +++ b/themes/weta_fade_chrome/README.md @@ -0,0 +1,9 @@ +# Themes: Animated + +## What it does + +Employs an PNG image as the Chrome compatible theme_frame image in a theme. + +## What it shows + +How to use the Chrome compatible keys in a theme's manifest.json file to make a theme available for Firefox and Chrome. diff --git a/themes/weta_fade_chrome/manifest.json b/themes/weta_fade_chrome/manifest.json new file mode 100755 index 0000000..ea0d35d --- /dev/null +++ b/themes/weta_fade_chrome/manifest.json @@ -0,0 +1,21 @@ +{ + + "description": "Version of the weta_fade theme using the Chrome compatible manifest keys. See https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Examples#themes", + "manifest_version": 2, + "name": "weta_fade_chrome", + "version": "1.0", + "homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/themes/weta_fade_chrome", + + + "theme": { + "images": { + "theme_frame": "weta.png" + }, + + "colors": { + "frame": [ 173 , 176 , 159 ], + "tab_text": [ 0 , 0 , 0 ] + } + } + +} diff --git a/themes/weta_fade_chrome/weta.png b/themes/weta_fade_chrome/weta.png new file mode 100755 index 0000000..8b79595 Binary files /dev/null and b/themes/weta_fade_chrome/weta.png differ diff --git a/themes/weta_mirror/README.md b/themes/weta_mirror/README.md new file mode 100644 index 0000000..7610d42 --- /dev/null +++ b/themes/weta_mirror/README.md @@ -0,0 +1,9 @@ +# Themes: Animated + +## What it does + +Employs two PNG images to create a mirror effect with the theme images. + +## What it shows + +How to use "additional_backgrounds": in conjunction with "additional_backgrounds_alignment": to place multiple images within the browser header. diff --git a/themes/weta_mirror/empty.png b/themes/weta_mirror/empty.png new file mode 100755 index 0000000..ceafa98 Binary files /dev/null and b/themes/weta_mirror/empty.png differ diff --git a/themes/weta_mirror/manifest.json b/themes/weta_mirror/manifest.json new file mode 100755 index 0000000..7a6749d --- /dev/null +++ b/themes/weta_mirror/manifest.json @@ -0,0 +1,24 @@ +{ + + "description": "Theme using multiple additional_backgrounds images. See https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Examples#themes", + "manifest_version": 2, + "name": "weta_mirror", + "version": "1.0", + "homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/themes/weta_mirror", + + "theme": { + "images": { + "headerURL": "empty.png", + "additional_backgrounds": [ "weta.png", "weta-left.png"] + }, + + "properties": { + "additional_backgrounds_alignment": [ "right top" , "left top" ] + }, + + "colors": { + "accentcolor": "#adb09f", + "textcolor": "#000" + } + } +} diff --git a/themes/weta_mirror/weta-left.png b/themes/weta_mirror/weta-left.png new file mode 100755 index 0000000..1c7d816 Binary files /dev/null and b/themes/weta_mirror/weta-left.png differ diff --git a/themes/weta_mirror/weta.png b/themes/weta_mirror/weta.png new file mode 100755 index 0000000..8b79595 Binary files /dev/null and b/themes/weta_mirror/weta.png differ diff --git a/themes/weta_tiled/README.md b/themes/weta_tiled/README.md new file mode 100644 index 0000000..11d539d --- /dev/null +++ b/themes/weta_tiled/README.md @@ -0,0 +1,9 @@ +# Themes: Animated + +## What it does + +Tiles a PNG imagein the browser header. + +## What it shows + +How to use "additional_backgrounds": in conjunction with "additional_backgrounds_alignment": and "additional_backgrounds_tiling": to place an image within the browser header and then tile it. diff --git a/themes/weta_tiled/empty.png b/themes/weta_tiled/empty.png new file mode 100755 index 0000000..ceafa98 Binary files /dev/null and b/themes/weta_tiled/empty.png differ diff --git a/themes/weta_tiled/manifest.json b/themes/weta_tiled/manifest.json new file mode 100644 index 0000000..347b65c --- /dev/null +++ b/themes/weta_tiled/manifest.json @@ -0,0 +1,25 @@ +{ + + "description": "Theme with a single image placed centrally and then tiled. See https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Examples#themes", + "manifest_version": 2, + "name": "weta_tiled", + "version": "1.0", + "homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/themes/weta_tiled", + + "theme": { + "images": { + "headerURL": "empty.png", + "additional_backgrounds": [ "weta_for_tiling.png"] + }, + + "properties": { + "additional_backgrounds_alignment": [ "top" ], + "additional_backgrounds_tiling": [ "repeat" ] + }, + + "colors": { + "accentcolor": "#adb09f", + "textcolor": "#000" + } + } +} diff --git a/themes/weta_tiled/weta_for_tiling.png b/themes/weta_tiled/weta_for_tiling.png new file mode 100755 index 0000000..467a548 Binary files /dev/null and b/themes/weta_tiled/weta_for_tiling.png differ diff --git a/user-agent-rewriter/popup/choose_ua.js b/user-agent-rewriter/popup/choose_ua.js index baaddaf..91dbbc9 100644 --- a/user-agent-rewriter/popup/choose_ua.js +++ b/user-agent-rewriter/popup/choose_ua.js @@ -4,7 +4,7 @@ If the user clicks on an element which has the class "ua-choice": * fetch the element's textContent: for example, "IE 11" * pass it into the background page's setUaString() function */ -document.addEventListener("click", function(e) { +document.addEventListener("click", (e) => { if (!e.target.classList.contains("ua-choice")) { return; } diff --git a/webpack-modules/.eslintrc.json b/webpack-modules/.eslintrc.json new file mode 100644 index 0000000..30e26e9 --- /dev/null +++ b/webpack-modules/.eslintrc.json @@ -0,0 +1,8 @@ +{ + "env": { + "browser": true, + "es6": true, + "amd": true, + "webextensions": true + } +} diff --git a/window-manipulator/window.html b/window-manipulator/window.html index c1ba472..f1ebd95 100644 --- a/window-manipulator/window.html +++ b/window-manipulator/window.html @@ -18,6 +18,10 @@
      + Preface title
      + +
      + Create new incognito window
      Create normal window
      Create panel
      diff --git a/window-manipulator/window.js b/window-manipulator/window.js index d6e36eb..b7e9396 100644 --- a/window-manipulator/window.js +++ b/window-manipulator/window.js @@ -26,48 +26,48 @@ document.addEventListener("click", (e) => { } else if (e.target.id === "window-create-normal") { - var createData = {}; - var creating = browser.windows.create(createData); + let createData = {}; + let creating = browser.windows.create(createData); creating.then(() => { console.log("The normal window has been created"); }); } else if (e.target.id === "window-create-incognito") { - var createData = { + let createData = { incognito: true, }; - var creating = browser.windows.create(createData); + let creating = browser.windows.create(createData); creating.then(() => { console.log("The incognito window has been created"); }); } else if (e.target.id === "window-create-panel") { - var createData = { + let createData = { type: "panel", }; - var creating = browser.windows.create(createData); + let creating = browser.windows.create(createData); creating.then(() => { console.log("The panel has been created"); }); } else if (e.target.id === "window-create-detached-panel") { - var createData = { + let createData = { type: "detached_panel", }; - var creating = browser.windows.create(createData); + let creating = browser.windows.create(createData); creating.then(() => { console.log("The detached panel has been created"); }); } else if (e.target.id === "window-create-popup") { - var createData = { + let createData = { type: "popup", }; - var creating = browser.windows.create(createData); + let creating = browser.windows.create(createData); creating.then(() => { console.log("The popup has been created"); }); @@ -92,5 +92,14 @@ document.addEventListener("click", (e) => { }); } + else if (e.target.id === "window-preface-title") { + getCurrentWindow().then((currentWindow) => { + let updateInfo = { + titlePreface: "Preface | " + } + browser.windows.update(currentWindow.id, updateInfo); + }); + } + e.preventDefault(); });