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/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..d358b2d
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,4 @@
+Code of conduct
+===============
+
+This repository is governed by Mozilla's code of conduct and etiquette guidelines. For more details please see the [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/) and [Developer Etiquette Guidelines](https://bugzilla.mozilla.org/page.cgi?id=etiquette.html).
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index db9d82f..4647afd 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -12,15 +12,13 @@ to help contributors write useful examples.
There are many ways you can help improve this repository! For example:
* **write a brand-new example:** for example, you might notice that there are no
-examples highlighting the [cookies API](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/cookies).
+examples highlighting a particular [API](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API).
* **extend an existing example:** for example,
-you might notice that the [tabs-tabs-tabs](https://github.com/mdn/webextensions-examples/tree/master/tabs-tabs-tabs) example
-uses a lot of tab manipulation APIs, but does not cover
-[tabs.setZoom()](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/tabs/setZoom).
+you might notice that an existing example could usefully be extended to demonstrate some extra APIs or techniques.
* **fix a bug:** we have a list of [issues](https://github.com/mdn/webextensions-examples/issues),
or maybe you found your own.
* **contribute a translation:** find an example with a `_locales` directory in
-it, and create a translation of the example's localizable strings into a new language.
+it, and create a translation of the example's localizable strings into a new language.
## Guidelines for examples
@@ -35,9 +33,17 @@ complexity
* [`description`](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/manifest.json/description)
* [`icons`](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/manifest.json/icons)
* `homepage_url`
- * [`strict_min_version` in the `applications` key](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/manifest.json/applications)
+* omit the [`applications` key](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/manifest.json/applications), unless either of the following apply:
+ * the example uses an API or other feature that's not yet available in the current released version of Firefox. In this case, include the `applications` key and set `strict_min_version` to the minimum required version of Firefox.
+ * the example needs an explicitly specified ID (for example, [native messaging](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Native_messaging) requires an explicitly specified ID). In this case, include the `applications` key and set `id` appropriately.
-Finally, note that the examples are all made available under the
+## Code style
+
+If you're editing an existing file, code style should be consistent with the rest of the code in the file. Otherwise, code style should follow the style for WebExtensions code itself: [https://wiki.mozilla.org/WebExtensions/Hacking#Code_Style](https://wiki.mozilla.org/WebExtensions/Hacking#Code_Style).
+
+## Licensing
+
+Please note that the examples are all made available under the
[Mozilla Public License 2.0](https://github.com/mdn/webextensions-examples/blob/master/LICENSE),
so any contributions must be
[compatible with that license](https://www.mozilla.org/en-US/MPL/license-policy/).
diff --git a/README.md b/README.md
index 37b39b8..426b57a 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# webextensions-examples
+# webextensions-examples [](https://travis-ci.org/mdn/webextensions-examples)
[https://github.com/mdn/webextensions-examples](https://github.com/mdn/webextensions-examples)
@@ -22,7 +22,7 @@ The examples are made available under the
To use the repository, first clone it.
Each example is in its own top-level directory. Install an example in your
-favourite web browser (see [installation instructions for Firefox](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Packaging_and_installation)),
+favourite web browser ([installation instructions](#installing-an-example) are below),
and see how it works. Each example has its own short README explaining what
it does.
@@ -30,11 +30,186 @@ To find your way around a WebExtension's internal structure, have a look at the
[Anatomy of a WebExtension](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Anatomy_of_a_WebExtension)
page on MDN.
-To use these examples in Firefox, you need at least Firefox 45. Some examples
-rely on APIs that were added in more recent versions of Firefox.
-To check the minimum version of Firefox needed for a given example,
-see the `strict_min_version` part of the [applications key](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/manifest.json/applications)
-in the example's manifest.json file.
+To use these examples in Firefox, you should use the most recent release
+of Firefox. Some examples work with earlier releases.
+
+A few examples rely on APIs that are currently only available in pre-release
+versions of Firefox. Where this is the case, the example should declare
+the minimum version that it needs in the `strict_min_version` part of the
+[applications key](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/manifest.json/applications)
+in its manifest.json file.
+
+## Installing an example
+
+There are a couple ways to try out the example extensions in this repository.
+
+1. Open Firefox and load `about:debugging` in the URL bar. Click the
+ [Load Temporary Add-on](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Temporary_Installation_in_Firefox)
+ button and select the `manifest.json` file within the
+ directory of an example extension you'd like to install.
+ Here is a [video](https://www.youtube.com/watch?v=cer9EUKegG4)
+ that demonstrates how to do this.
+2. Install the
+ [web-ext](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Getting_started_with_web-ext)
+ tool, change into the directory of the example extension
+ you'd like to install, and type `web-ext run`. This will launch Firefox and
+ install the extension automatically. This tool gives you some
+ additional development features such as
+ [automatic reloading](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Getting_started_with_web-ext#Automatic_extension_reloading).
+
+## Index of examples
+
+
Demo of various windows functions: create, close, resize, etc.
+
## Learn more
@@ -53,4 +228,3 @@ If you need help, email the [dev-addons mailing list](https://mail.mozilla.org/l
We welcome contributions, whether they are whole new examples, new features,
bug fixes, or translations of localizable strings into new languages. Please
see the [CONTRIBUTING.md](https://github.com/mdn/webextensions-examples/blob/master/CONTRIBUTING.md) file for more details.
-
diff --git a/annotate-page/README.md b/annotate-page/README.md
new file mode 100644
index 0000000..ca1abf4
--- /dev/null
+++ b/annotate-page/README.md
@@ -0,0 +1,11 @@
+# annotate-page
+
+## What it does
+
+This example adds a sidebar that lets you take notes on the current web page. The notes are saved to local storage, and the notes for each page are shown again when you open that page again.
+
+The example also uses the `commands` manifest key to add a keyboard shortcut that opens the sidebar.
+
+## What it shows
+
+How to create a sidebar for an add-on. How to associate the sidebar with the currently active tab in that sidebar's window. How to store and restore sidebar content.
diff --git a/annotate-page/icons/star.png b/annotate-page/icons/star.png
new file mode 100644
index 0000000..f8edd21
Binary files /dev/null and b/annotate-page/icons/star.png differ
diff --git a/annotate-page/manifest.json b/annotate-page/manifest.json
new file mode 100644
index 0000000..6edf05c
--- /dev/null
+++ b/annotate-page/manifest.json
@@ -0,0 +1,29 @@
+{
+
+ "manifest_version": 2,
+ "name": "Web page annotator",
+ "description": "Displays a sidebar that lets you take notes on web pages.",
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "54.0a1"
+ }
+ },
+
+ "sidebar_action": {
+ "default_icon": "icons/star.png",
+ "default_title" : "Annotator",
+ "default_panel": "sidebar/panel.html"
+ },
+
+ "permissions": ["storage", "tabs"],
+
+ "commands": {
+ "_execute_sidebar_action": {
+ "suggested_key": {
+ "default": "Ctrl+Shift+Y"
+ }
+ }
+ }
+
+}
diff --git a/annotate-page/sidebar/panel.css b/annotate-page/sidebar/panel.css
new file mode 100644
index 0000000..d2ceb02
--- /dev/null
+++ b/annotate-page/sidebar/panel.css
@@ -0,0 +1,32 @@
+html, body {
+ height: 100%;
+ width: 100%;
+ margin: 0;
+ box-sizing: border-box;
+}
+
+body {
+ height: 90%;
+ font: caption;
+ background-color: #F4F7F8;
+}
+
+p {
+ margin: 1em 2em;
+}
+
+#content {
+ height: 90%;
+ margin: 2em 2em 0 2em;
+ border: .5em solid #DDE4E9;
+ transition: background-color .2s ease-out;
+}
+
+#content[contenteditable=true] {
+ background-color: white;
+ transition: background-color .2s ease-in;
+}
+
+[contenteditable]:focus {
+ outline: 0px solid transparent;
+}
diff --git a/annotate-page/sidebar/panel.html b/annotate-page/sidebar/panel.html
new file mode 100644
index 0000000..0f900c7
--- /dev/null
+++ b/annotate-page/sidebar/panel.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
Click inside the box to start taking notes on this page.
+
+
+
+
diff --git a/annotate-page/sidebar/panel.js b/annotate-page/sidebar/panel.js
new file mode 100644
index 0000000..2af8505
--- /dev/null
+++ b/annotate-page/sidebar/panel.js
@@ -0,0 +1,57 @@
+var myWindowId;
+const contentBox = document.querySelector("#content");
+
+/*
+Make the content box editable as soon as the user mouses over the sidebar.
+*/
+window.addEventListener("mouseover", () => {
+ contentBox.setAttribute("contenteditable", true);
+});
+
+/*
+When the user mouses out, save the current contents of the box.
+*/
+window.addEventListener("mouseout", () => {
+ contentBox.setAttribute("contenteditable", false);
+ browser.tabs.query({windowId: myWindowId, active: true}).then((tabs) => {
+ let contentToStore = {};
+ contentToStore[tabs[0].url] = contentBox.textContent;
+ browser.storage.local.set(contentToStore);
+ });
+});
+
+/*
+Update the sidebar's content.
+
+1) Get the active tab in this sidebar's window.
+2) Get its stored content.
+3) Put it in the content box.
+*/
+function updateContent() {
+ browser.tabs.query({windowId: myWindowId, active: true})
+ .then((tabs) => {
+ return browser.storage.local.get(tabs[0].url);
+ })
+ .then((storedInfo) => {
+ contentBox.textContent = storedInfo[Object.keys(storedInfo)[0]];
+ });
+}
+
+/*
+Update content when a new tab becomes active.
+*/
+browser.tabs.onActivated.addListener(updateContent);
+
+/*
+Update content when a new page is loaded into a tab.
+*/
+browser.tabs.onUpdated.addListener(updateContent);
+
+/*
+When the sidebar loads, get the ID of its window,
+and update its content.
+*/
+browser.windows.getCurrent({populate: true}).then((windowInfo) => {
+ myWindowId = windowInfo.id;
+ updateContent();
+});
diff --git a/apply-css/README.md b/apply-css/README.md
new file mode 100644
index 0000000..c027dde
--- /dev/null
+++ b/apply-css/README.md
@@ -0,0 +1,21 @@
+# apply-css
+
+**This add-on injects CSS 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
+
+This extension includes:
+
+* a background script, "background.js"
+* a page action
+
+It adds the [page action](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/pageAction)
+to every tab the user opens. Clicking the page action
+for a tab applies some CSS using [tabs.insertCSS](https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/API/tabs/insertCSS).
+
+Clicking again removes the CSS using [tabs.removeCSS](https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/API/tabs/removeCSS).
+
+## What it shows
+
+* some basic page action functions
+* how to apply and remove CSS.
diff --git a/apply-css/background.js b/apply-css/background.js
new file mode 100644
index 0000000..3c0b1aa
--- /dev/null
+++ b/apply-css/background.js
@@ -0,0 +1,69 @@
+const CSS = "body { border: 20px solid red; }";
+const TITLE_APPLY = "Apply CSS";
+const TITLE_REMOVE = "Remove CSS";
+const APPLICABLE_PROTOCOLS = ["http:", "https:"];
+
+/*
+Toggle CSS: based on the current title, insert or remove the CSS.
+Update the page action's title and icon to reflect its state.
+*/
+function toggleCSS(tab) {
+
+ function gotTitle(title) {
+ if (title === TITLE_APPLY) {
+ browser.pageAction.setIcon({tabId: tab.id, path: "icons/on.svg"});
+ browser.pageAction.setTitle({tabId: tab.id, title: TITLE_REMOVE});
+ browser.tabs.insertCSS({code: CSS});
+ } else {
+ browser.pageAction.setIcon({tabId: tab.id, path: "icons/off.svg"});
+ browser.pageAction.setTitle({tabId: tab.id, title: TITLE_APPLY});
+ browser.tabs.removeCSS({code: CSS});
+ }
+ }
+
+ var gettingTitle = browser.pageAction.getTitle({tabId: tab.id});
+ gettingTitle.then(gotTitle);
+}
+
+/*
+Returns true only if the URL's protocol is in APPLICABLE_PROTOCOLS.
+*/
+function protocolIsApplicable(url) {
+ var anchor = document.createElement('a');
+ anchor.href = url;
+ return APPLICABLE_PROTOCOLS.includes(anchor.protocol);
+}
+
+/*
+Initialize the page action: set icon and title, then show.
+Only operates on tabs whose URL's protocol is applicable.
+*/
+function initializePageAction(tab) {
+ if (protocolIsApplicable(tab.url)) {
+ browser.pageAction.setIcon({tabId: tab.id, path: "icons/off.svg"});
+ browser.pageAction.setTitle({tabId: tab.id, title: TITLE_APPLY});
+ browser.pageAction.show(tab.id);
+ }
+}
+
+/*
+When first loaded, initialize the page action for all tabs.
+*/
+var gettingAllTabs = browser.tabs.query({});
+gettingAllTabs.then((tabs) => {
+ for (let tab of tabs) {
+ initializePageAction(tab);
+ }
+});
+
+/*
+Each time a tab is updated, reset the page action for that tab.
+*/
+browser.tabs.onUpdated.addListener((id, changeInfo, tab) => {
+ initializePageAction(tab);
+});
+
+/*
+Toggle CSS when the page action is clicked.
+*/
+browser.pageAction.onClicked.addListener(toggleCSS);
diff --git a/apply-css/icons/LICENSE b/apply-css/icons/LICENSE
new file mode 100644
index 0000000..b6c3bc7
--- /dev/null
+++ b/apply-css/icons/LICENSE
@@ -0,0 +1,2 @@
+These icons are taken from the "Material Design" iconset designed by Google:
+https://google.github.io/material-design-icons/.
diff --git a/apply-css/icons/off.svg b/apply-css/icons/off.svg
new file mode 100644
index 0000000..e54ed1c
--- /dev/null
+++ b/apply-css/icons/off.svg
@@ -0,0 +1 @@
+
diff --git a/apply-css/icons/on.svg b/apply-css/icons/on.svg
new file mode 100644
index 0000000..d9a0cb3
--- /dev/null
+++ b/apply-css/icons/on.svg
@@ -0,0 +1 @@
+
diff --git a/apply-css/manifest.json b/apply-css/manifest.json
new file mode 100644
index 0000000..f200916
--- /dev/null
+++ b/apply-css/manifest.json
@@ -0,0 +1,23 @@
+{
+
+ "description": "Adds a page action to toggle applying CSS to pages.",
+ "manifest_version": 2,
+ "name": "apply-css",
+ "version": "1.0",
+ "homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/apply-css",
+
+ "background": {
+ "scripts": ["background.js"]
+ },
+
+ "page_action": {
+ "default_icon": "icons/off.svg",
+ "browser_style": true
+ },
+
+ "permissions": [
+ "activeTab",
+ "tabs"
+ ]
+
+}
diff --git a/beastify/README.md b/beastify/README.md
index 26443f1..941189a 100644
--- a/beastify/README.md
+++ b/beastify/README.md
@@ -1,5 +1,7 @@
# beastify
+**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:
@@ -18,6 +20,8 @@ the name of the chosen beast.
When the content script receives this message, it replaces the current page
content with an image of the chosen beast.
+When the user clicks the reset button, the page reloads, and reverts to its original form.
+
## What it shows ##
* write a browser action with a popup
@@ -25,3 +29,4 @@ content with an image of the chosen beast.
* inject a content script programmatically using `tabs.executeScript()`
* send a message from the main extension to a content script
* use web accessible resources to enable web pages to load packaged content
+* reload web pages
diff --git a/beastify/content_scripts/beastify.js b/beastify/content_scripts/beastify.js
index d10cca0..88f3f71 100644
--- a/beastify/content_scripts/beastify.js
+++ b/beastify/content_scripts/beastify.js
@@ -7,7 +7,7 @@ beastify():
function beastify(request, sender, sendResponse) {
removeEverything();
insertBeast(request.beastURL);
- chrome.runtime.onMessage.removeListener(beastify);
+ browser.runtime.onMessage.removeListener(beastify);
}
/*
@@ -34,4 +34,4 @@ function insertBeast(beastURL) {
/*
Assign beastify() as a listener for messages from the extension.
*/
-chrome.runtime.onMessage.addListener(beastify);
+browser.runtime.onMessage.addListener(beastify);
diff --git a/beastify/manifest.json b/beastify/manifest.json
index da4abc8..e8bf86b 100644
--- a/beastify/manifest.json
+++ b/beastify/manifest.json
@@ -9,13 +9,6 @@
"48": "icons/beasts-48.png"
},
- "applications": {
- "gecko": {
- "id": "beastify@mozilla.org",
- "strict_min_version": "45.0"
- }
- },
-
"permissions": [
"activeTab"
],
diff --git a/beastify/popup/choose_beast.css b/beastify/popup/choose_beast.css
index e439cb0..36c1068 100644
--- a/beastify/popup/choose_beast.css
+++ b/beastify/popup/choose_beast.css
@@ -2,15 +2,26 @@ html, body {
width: 100px;
}
-.beast {
+.button {
margin: 3% auto;
padding: 4px;
text-align: center;
font-size: 1.5em;
- background-color: #E5F2F2;
cursor: pointer;
}
.beast:hover {
background-color: #CFF2F2;
}
+
+.beast {
+ background-color: #E5F2F2;
+}
+
+.clear {
+ background-color: #FBFBC9;
+}
+
+.clear:hover {
+ background-color: #EAEA9D;
+}
diff --git a/beastify/popup/choose_beast.html b/beastify/popup/choose_beast.html
index 47bc5e3..5984a4e 100644
--- a/beastify/popup/choose_beast.html
+++ b/beastify/popup/choose_beast.html
@@ -7,9 +7,10 @@
-
Frog
-
Turtle
-
Snake
+
Frog
+
Turtle
+
Snake
+
Reset
diff --git a/beastify/popup/choose_beast.js b/beastify/popup/choose_beast.js
index e282e4f..d70225c 100644
--- a/beastify/popup/choose_beast.js
+++ b/beastify/popup/choose_beast.js
@@ -4,41 +4,45 @@ Given the name of a beast, get the URL to the corresponding image.
function beastNameToURL(beastName) {
switch (beastName) {
case "Frog":
- return chrome.extension.getURL("beasts/frog.jpg");
+ return browser.extension.getURL("beasts/frog.jpg");
case "Snake":
- return chrome.extension.getURL("beasts/snake.jpg");
+ return browser.extension.getURL("beasts/snake.jpg");
case "Turtle":
- return chrome.extension.getURL("beasts/turtle.jpg");
+ return browser.extension.getURL("beasts/turtle.jpg");
}
}
-
/*
Listen for clicks in the popup.
-If the click is not on one of the beasts, return early.
+If the click is on one of the beasts:
+ Inject the "beastify.js" content script in the active tab.
-Otherwise, the text content of the node is the name of the beast we want.
+ Then get the active tab and send "beastify.js" a message
+ containing the URL to the chosen beast's image.
-Inject the "beastify.js" content script in the active tab.
-
-Then get the active tab and send "beastify.js" a message
-containing the URL to the chosen beast's image.
+If it's on a button wich contains class "clear":
+ Reload the page.
+ Close the popup. This is needed, as the content script malfunctions after page reloads.
*/
-document.addEventListener("click", function(e) {
- if (!e.target.classList.contains("beast")) {
+document.addEventListener("click", (e) => {
+ if (e.target.classList.contains("beast")) {
+ var chosenBeast = e.target.textContent;
+ var chosenBeastURL = beastNameToURL(chosenBeast);
+
+ browser.tabs.executeScript(null, {
+ file: "/content_scripts/beastify.js"
+ });
+
+ var gettingActiveTab = browser.tabs.query({active: true, currentWindow: true});
+ gettingActiveTab.then((tabs) => {
+ browser.tabs.sendMessage(tabs[0].id, {beastURL: chosenBeastURL});
+ });
+ }
+ else if (e.target.classList.contains("clear")) {
+ browser.tabs.reload();
+ window.close();
+
return;
}
-
- var chosenBeast = e.target.textContent;
- var chosenBeastURL = beastNameToURL(chosenBeast);
-
- chrome.tabs.executeScript(null, {
- file: "/content_scripts/beastify.js"
- });
-
- chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
- chrome.tabs.sendMessage(tabs[0].id, {beastURL: chosenBeastURL});
- });
-
});
diff --git a/bookmark-it/README.md b/bookmark-it/README.md
index 28c87e0..e2d90cb 100644
--- a/bookmark-it/README.md
+++ b/bookmark-it/README.md
@@ -1,7 +1,5 @@
# bookmark-it
-> This example uses APIs that are available from Firefox 47 onwards.
-
## What it does
Displays a simple button in the menu bar that toggles a bookmark for the currently active tab.
diff --git a/bookmark-it/background.js b/bookmark-it/background.js
index 6b07a58..565fa06 100644
--- a/bookmark-it/background.js
+++ b/bookmark-it/background.js
@@ -6,7 +6,7 @@ var currentBookmark;
* is already bookmarked.
*/
function updateIcon() {
- chrome.browserAction.setIcon({
+ browser.browserAction.setIcon({
path: currentBookmark ? {
19: "icons/star-filled-19.png",
38: "icons/star-filled-38.png"
@@ -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
+ });
}
/*
@@ -23,42 +28,59 @@ function updateIcon() {
*/
function toggleBookmark() {
if (currentBookmark) {
- chrome.bookmarks.remove(currentBookmark.id);
- currentBookmark = null;
- updateIcon();
+ browser.bookmarks.remove(currentBookmark.id);
} else {
- chrome.bookmarks.create({title: currentTab.title, url: currentTab.url}, function(bookmark) {
- currentBookmark = bookmark;
- updateIcon();
- });
+ browser.bookmarks.create({title: currentTab.title, url: currentTab.url});
}
}
-chrome.browserAction.onClicked.addListener(toggleBookmark);
+browser.browserAction.onClicked.addListener(toggleBookmark);
/*
* Switches currentTab and currentBookmark to reflect the currently active tab
*/
-function updateTab() {
- chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
+function updateActiveTab(tabs) {
+
+ function isSupportedProtocol(urlString) {
+ var supportedProtocols = ["https:", "http:", "ftp:", "file:"];
+ var url = document.createElement('a');
+ url.href = urlString;
+ return supportedProtocols.indexOf(url.protocol) != -1;
+ }
+
+ function updateTab(tabs) {
if (tabs[0]) {
currentTab = tabs[0];
-
- chrome.bookmarks.search({url: currentTab.url}, (bookmarks) => {
- currentBookmark = bookmarks[0];
- updateIcon();
- });
+ if (isSupportedProtocol(currentTab.url)) {
+ var searching = browser.bookmarks.search({url: currentTab.url});
+ searching.then((bookmarks) => {
+ currentBookmark = bookmarks[0];
+ updateIcon();
+ });
+ } else {
+ console.log(`Bookmark it! does not support the '${currentTab.url}' URL.`)
+ }
}
- });
+ }
+
+ var gettingActiveTab = browser.tabs.query({active: true, currentWindow: true});
+ gettingActiveTab.then(updateTab);
}
-// TODO listen for bookmarks.onCreated and bookmarks.onRemoved once Bug 1221764 lands
+// listen for bookmarks being created
+browser.bookmarks.onCreated.addListener(updateActiveTab);
+
+// listen for bookmarks being removed
+browser.bookmarks.onRemoved.addListener(updateActiveTab);
// listen to tab URL changes
-chrome.tabs.onUpdated.addListener(updateTab);
+browser.tabs.onUpdated.addListener(updateActiveTab);
// listen to tab switching
-chrome.tabs.onActivated.addListener(updateTab);
+browser.tabs.onActivated.addListener(updateActiveTab);
+
+// listen for window switching
+browser.windows.onFocusChanged.addListener(updateActiveTab);
// update when the extension loads initially
-updateTab();
+updateActiveTab();
diff --git a/bookmark-it/manifest.json b/bookmark-it/manifest.json
index 99a8eae..017a650 100644
--- a/bookmark-it/manifest.json
+++ b/bookmark-it/manifest.json
@@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Bookmark it!",
- "version": "1.0",
+ "version": "1.1",
"description": "A simple bookmark button",
"homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/bookmark-it",
"icons": {
@@ -9,13 +9,6 @@
"96": "icons/bookmark-it@2x.png"
},
- "applications": {
- "gecko": {
- "id": "bookmark-it-extension@mozilla.org",
- "strict_min_version": "47.0a1"
- }
- },
-
"permissions": [
"bookmarks",
"tabs"
diff --git a/borderify/README.md b/borderify/README.md
index ea3b498..1624883 100644
--- a/borderify/README.md
+++ b/borderify/README.md
@@ -1,5 +1,7 @@
# borderify
+**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
This extension just includes:
diff --git a/borderify/manifest.json b/borderify/manifest.json
index c0b1714..0c44577 100644
--- a/borderify/manifest.json
+++ b/borderify/manifest.json
@@ -9,13 +9,6 @@
"48": "icons/border-48.png"
},
- "applications": {
- "gecko": {
- "id": "borderify@mozilla.org",
- "strict_min_version": "45.0"
- }
- },
-
"content_scripts": [
{
"matches": ["*://*.mozilla.org/*"],
diff --git a/build/README.md b/build/README.md
new file mode 100644
index 0000000..bb970aa
--- /dev/null
+++ b/build/README.md
@@ -0,0 +1,13 @@
+This is a built version of most of the extensions in this repository. It excludes:
+
+* webpack-modules: because this one is all about building the module using webpack
+
+Because the extensions mostly have no application id, they can be signed by anyone on addons.mozilla.org. We haven't checked in or stored those ids so you can rebuild them if you'd like. We don't expect end users to be expecting these examples to update automatically (which is what the id is for).
+
+A couple do have ids. You can still sign them yourself by changing the application id. If you'd like to sign them officially and add them to this repo, contact @andymckay or @wbamberg and they can do it for you.
+
+If an extension changes and you'd like to rebuild it, then you should probably update the version number before building it.
+
+To build a new version of the extension use the web-ext sign command, for example:
+
+ pushd ../commands/ && web-ext sign --artifacts-dir ../build && popd
diff --git a/build/apply_css-1.0-an+fx.xpi b/build/apply_css-1.0-an+fx.xpi
new file mode 100644
index 0000000..7830608
Binary files /dev/null and b/build/apply_css-1.0-an+fx.xpi differ
diff --git a/build/beastify-1.0-an+fx.xpi b/build/beastify-1.0-an+fx.xpi
new file mode 100644
index 0000000..23a317f
Binary files /dev/null and b/build/beastify-1.0-an+fx.xpi differ
diff --git a/build/bookmark_it-1.0-an+fx.xpi b/build/bookmark_it-1.0-an+fx.xpi
new file mode 100644
index 0000000..cea19a0
Binary files /dev/null and b/build/bookmark_it-1.0-an+fx.xpi differ
diff --git a/build/borderify-1.0-an+fx.xpi b/build/borderify-1.0-an+fx.xpi
new file mode 100644
index 0000000..365ba75
Binary files /dev/null and b/build/borderify-1.0-an+fx.xpi differ
diff --git a/build/chillout_page_action-1.0-an+fx.xpi b/build/chillout_page_action-1.0-an+fx.xpi
new file mode 100644
index 0000000..a71dd01
Binary files /dev/null and b/build/chillout_page_action-1.0-an+fx.xpi differ
diff --git a/build/context_menu_demo-1.0-an+fx.xpi b/build/context_menu_demo-1.0-an+fx.xpi
new file mode 100644
index 0000000..4fd3d23
Binary files /dev/null and b/build/context_menu_demo-1.0-an+fx.xpi differ
diff --git a/build/cookie_bg_picker-1.0-an+fx.xpi b/build/cookie_bg_picker-1.0-an+fx.xpi
new file mode 100644
index 0000000..24abd96
Binary files /dev/null and b/build/cookie_bg_picker-1.0-an+fx.xpi differ
diff --git a/build/emoji_substitution-1.0-an+fx.xpi b/build/emoji_substitution-1.0-an+fx.xpi
new file mode 100644
index 0000000..3e1a2c8
Binary files /dev/null and b/build/emoji_substitution-1.0-an+fx.xpi differ
diff --git a/build/eslint_example-1.0-an+fx.xpi b/build/eslint_example-1.0-an+fx.xpi
new file mode 100644
index 0000000..2388674
Binary files /dev/null and b/build/eslint_example-1.0-an+fx.xpi differ
diff --git a/build/favourite_colour-1.0-an+fx.xpi b/build/favourite_colour-1.0-an+fx.xpi
new file mode 100644
index 0000000..3b10bae
Binary files /dev/null and b/build/favourite_colour-1.0-an+fx.xpi differ
diff --git a/build/history_deleter-1.0-an+fx.xpi b/build/history_deleter-1.0-an+fx.xpi
new file mode 100644
index 0000000..080423c
Binary files /dev/null and b/build/history_deleter-1.0-an+fx.xpi differ
diff --git a/build/latest_download-1.0-an+fx.xpi b/build/latest_download-1.0-an+fx.xpi
new file mode 100644
index 0000000..68d2f3c
Binary files /dev/null and b/build/latest_download-1.0-an+fx.xpi differ
diff --git a/build/list_cookies-1.0-an+fx.xpi b/build/list_cookies-1.0-an+fx.xpi
new file mode 100644
index 0000000..a34037a
Binary files /dev/null and b/build/list_cookies-1.0-an+fx.xpi differ
diff --git a/build/navigation_stats-0.1.0-an+fx.xpi b/build/navigation_stats-0.1.0-an+fx.xpi
new file mode 100644
index 0000000..c62dc93
Binary files /dev/null and b/build/navigation_stats-0.1.0-an+fx.xpi differ
diff --git a/build/notify_link_clicks_i18n-1.0-an+fx.xpi b/build/notify_link_clicks_i18n-1.0-an+fx.xpi
new file mode 100644
index 0000000..596b053
Binary files /dev/null and b/build/notify_link_clicks_i18n-1.0-an+fx.xpi differ
diff --git a/build/open_my_page-1.0-an+fx.xpi b/build/open_my_page-1.0-an+fx.xpi
new file mode 100644
index 0000000..9f7bcfd
Binary files /dev/null and b/build/open_my_page-1.0-an+fx.xpi differ
diff --git a/build/page_to_extension_messaging-1.0-an+fx.xpi b/build/page_to_extension_messaging-1.0-an+fx.xpi
new file mode 100644
index 0000000..476532f
Binary files /dev/null and b/build/page_to_extension_messaging-1.0-an+fx.xpi differ
diff --git a/build/page_to_extension_messaging-1.0-fx.xpi b/build/page_to_extension_messaging-1.0-fx.xpi
new file mode 100644
index 0000000..c73a004
Binary files /dev/null and b/build/page_to_extension_messaging-1.0-fx.xpi differ
diff --git a/build/quicknote-1.0-an+fx.xpi b/build/quicknote-1.0-an+fx.xpi
new file mode 100644
index 0000000..36de373
Binary files /dev/null and b/build/quicknote-1.0-an+fx.xpi differ
diff --git a/build/sample_commands_extension-1.0-an+fx.xpi b/build/sample_commands_extension-1.0-an+fx.xpi
new file mode 100644
index 0000000..9ed7873
Binary files /dev/null and b/build/sample_commands_extension-1.0-an+fx.xpi differ
diff --git a/build/sample_commands_extension-1.0-fx.xpi b/build/sample_commands_extension-1.0-fx.xpi
new file mode 100644
index 0000000..c604b5a
Binary files /dev/null and b/build/sample_commands_extension-1.0-fx.xpi differ
diff --git a/build/selection_to_clipboard-1.0-an+fx.xpi b/build/selection_to_clipboard-1.0-an+fx.xpi
new file mode 100644
index 0000000..b413272
Binary files /dev/null and b/build/selection_to_clipboard-1.0-an+fx.xpi differ
diff --git a/build/tab_strip-1.0-an+fx.xpi b/build/tab_strip-1.0-an+fx.xpi
new file mode 100644
index 0000000..60e3786
Binary files /dev/null and b/build/tab_strip-1.0-an+fx.xpi differ
diff --git a/build/tabs_tabs_tabs-1.0-an+fx.xpi b/build/tabs_tabs_tabs-1.0-an+fx.xpi
new file mode 100644
index 0000000..e913c2f
Binary files /dev/null and b/build/tabs_tabs_tabs-1.0-an+fx.xpi differ
diff --git a/build/user_agent_rewriter-1.0-an+fx.xpi b/build/user_agent_rewriter-1.0-an+fx.xpi
new file mode 100644
index 0000000..4385855
Binary files /dev/null and b/build/user_agent_rewriter-1.0-an+fx.xpi differ
diff --git a/build/window_manipulator-1.0-an+fx.xpi b/build/window_manipulator-1.0-an+fx.xpi
new file mode 100644
index 0000000..04350e3
Binary files /dev/null and b/build/window_manipulator-1.0-an+fx.xpi differ
diff --git a/chill-out/README.md b/chill-out/README.md
index 93c09ad..5332e13 100644
--- a/chill-out/README.md
+++ b/chill-out/README.md
@@ -2,8 +2,10 @@
## What it does
-After N seconds of inactivity (defined as, the user not having navigated
-or switched the active tab) display show a page action for that tab.
+After N seconds of inactivity (defined as the user not having navigated
+or switched away from the active tab) display a
+[page action](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/pageAction)
+for that tab.
When the user clicks the page action,
navigate to http://chilloutandwatchsomecatgifs.com/.
diff --git a/chill-out/background.js b/chill-out/background.js
index 696a76c..d29d949 100644
--- a/chill-out/background.js
+++ b/chill-out/background.js
@@ -14,18 +14,20 @@ var CATGIFS = "http://chilloutandwatchsomecatgifs.com/";
/*
Restart alarm for the currently active tab, whenever background.js is run.
*/
-chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
+var gettingActiveTab = browser.tabs.query({active: true, currentWindow: true});
+gettingActiveTab.then((tabs) => {
restartAlarm(tabs[0].id);
});
/*
Restart alarm for the currently active tab, whenever the user navigates.
*/
-chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
+browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (!changeInfo.url) {
return;
}
- chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
+ var gettingActiveTab = browser.tabs.query({active: true, currentWindow: true});
+ gettingActiveTab.then((tabs) => {
if (tabId == tabs[0].id) {
restartAlarm(tabId);
}
@@ -35,7 +37,7 @@ chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
/*
Restart alarm for the currently active tab, whenever a new tab becomes active.
*/
-chrome.tabs.onActivated.addListener(function (activeInfo) {
+browser.tabs.onActivated.addListener((activeInfo) => {
restartAlarm(activeInfo.tabId);
});
@@ -44,11 +46,12 @@ restartAlarm: clear all alarms,
then set a new alarm for the given tab.
*/
function restartAlarm(tabId) {
- chrome.pageAction.hide(tabId);
- chrome.alarms.clearAll();
- chrome.tabs.get(tabId, function(tab) {
+ browser.pageAction.hide(tabId);
+ browser.alarms.clearAll();
+ var gettingTab = browser.tabs.get(tabId);
+ gettingTab.then((tab) => {
if (tab.url != CATGIFS) {
- chrome.alarms.create("", {delayInMinutes: DELAY});
+ browser.alarms.create("", {delayInMinutes: DELAY});
}
});
}
@@ -56,15 +59,16 @@ function restartAlarm(tabId) {
/*
On alarm, show the page action.
*/
-chrome.alarms.onAlarm.addListener(function(alarm) {
- chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
- chrome.pageAction.show(tabs[0].id);
+browser.alarms.onAlarm.addListener((alarm) => {
+ var gettingActiveTab = browser.tabs.query({active: true, currentWindow: true});
+ gettingActiveTab.then((tabs) => {
+ browser.pageAction.show(tabs[0].id);
});
});
/*
On page action click, navigate the corresponding tab to the cat gifs.
*/
-chrome.pageAction.onClicked.addListener(function () {
- chrome.tabs.update({url: CATGIFS});
+browser.pageAction.onClicked.addListener(() => {
+ browser.tabs.update({url: CATGIFS});
});
diff --git a/chill-out/manifest.json b/chill-out/manifest.json
index 37bb6f1..13bc042 100644
--- a/chill-out/manifest.json
+++ b/chill-out/manifest.json
@@ -8,12 +8,6 @@
"48": "icons/chillout-48.png"
},
- "applications": {
- "gecko": {
- "id": "chillout-page-action@mozilla.org"
- }
- },
-
"permissions": [
"alarms",
"tabs"
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 f4d0b33..a54dc95 100644
--- a/commands/background.js
+++ b/commands/background.js
@@ -10,10 +10,11 @@
* shortcut: "Ctrl+Shift+U"
* }]
*/
-chrome.commands.getAll(function(commands) {
- commands.forEach(function(command) {
+var gettingAllCommands = browser.commands.getAll();
+gettingAllCommands.then((commands) => {
+ for (let command of commands) {
console.log(command);
- });
+ }
});
/**
@@ -22,6 +23,6 @@ chrome.commands.getAll(function(commands) {
* In this sample extension, there is only one registered command: "Ctrl+Shift+U".
* On Mac, this command will automatically be converted to "Command+Shift+U".
*/
-chrome.commands.onCommand.addListener(function(command) {
+browser.commands.onCommand.addListener((command) => {
console.log("onCommand event received for message: ", command);
});
diff --git a/commands/manifest.json b/commands/manifest.json
index 0ecbb22..785b9f0 100644
--- a/commands/manifest.json
+++ b/commands/manifest.json
@@ -7,12 +7,7 @@
"background": {
"scripts": ["background.js"]
},
- "applications": {
- "gecko": {
- "id": "commands@mozilla.org",
- "strict_min_version": "48.0a1"
- }
- },
+
"commands": {
"toggle-feature": {
"suggested_key": { "default": "Ctrl+Shift+U" },
diff --git a/context-menu-copy-link-with-types/README.md b/context-menu-copy-link-with-types/README.md
new file mode 100644
index 0000000..0d33607
--- /dev/null
+++ b/context-menu-copy-link-with-types/README.md
@@ -0,0 +1,35 @@
+# Context menu: Copy link with types
+
+This example adds a context menu item to every link that copies the URL to the
+clipboard, as plain text and as rich HTML.
+
+## What it does
+
+This extension includes:
+
+* a background script that:
+ - Registers a context menu item for every link.
+ - Upon click, it invokes the function to copy text and HTML to the clipboard.
+* a helper script, "clipboard-helper.js" that provides the copy-to-clipboard functionality.
+ In the example, this script is run as a content script, but the actual functionality can also
+ be used in visible extension pages such as extension button popups or extension tabs.
+* a page, "preview.html" for testing the effect of copying to the clipboard.
+ This page does not need to be part of the extension, and can directly be opened in the browser.
+
+To test the extension, right-click on any link to open a context menu, and choose the
+"Copy link to clipboard" option. Then open preview.html and paste the clipboard content
+in the two displayed boxes. The first box will display "This is text: ..." and the second
+box will display "This is HTML: ...".
+
+Note: since the add-on relies on a content script for copying the text, the copy operation
+will only succeed if the add-on is allowed to run scripts in the current page.
+If you wish to successfully copy the text even if the current page cannot be scripted, then
+you can open an (extension) page in a new tab as a fallback.
+
+## What it shows
+
+* how to put data on the [clipboard](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Interact_with_the_clipboard)
+ with custom types ("text/plain" and "text/html" in the example).
+* how to safely construct HTML from given text.
+* how to safely create JavaScript code to run as a dynamic content script.
+* how to dynamically run a static content script only once.
diff --git a/context-menu-copy-link-with-types/background.js b/context-menu-copy-link-with-types/background.js
new file mode 100644
index 0000000..9152745
--- /dev/null
+++ b/context-menu-copy-link-with-types/background.js
@@ -0,0 +1,54 @@
+browser.contextMenus.create({
+ id: "copy-link-to-clipboard",
+ title: "Copy link to clipboard",
+ contexts: ["link"],
+});
+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;
+ // Always HTML-escape external input to avoid XSS.
+ const safeUrl = escapeHTML(info.linkUrl);
+ const html = `This is HTML: ${safeUrl}`;
+
+ // The example will show how data can be copied, but since background
+ // pages cannot directly write to the clipboard, we will run a content
+ // script that copies the actual content.
+
+ // clipboard-helper.js defines function copyToClipboard.
+ const code = "copyToClipboard(" +
+ JSON.stringify(text) + "," +
+ JSON.stringify(html) + ");";
+
+ browser.tabs.executeScript({
+ code: "typeof copyToClipboard === 'function';",
+ }).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.
+ if (!results || results[0] !== true) {
+ return browser.tabs.executeScript(tab.id, {
+ file: "clipboard-helper.js",
+ });
+ }
+ }).then(() => {
+ return browser.tabs.executeScript(tab.id, {
+ code,
+ });
+ }).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);
+ });
+ }
+});
+
+// https://gist.github.com/Rob--W/ec23b9d6db9e56b7e4563f1544e0d546
+function escapeHTML(str) {
+ // Note: string cast using String; may throw if `str` is non-serializable, e.g. a Symbol.
+ // Most often this is not the case though.
+ return String(str)
+ .replace(/&/g, "&")
+ .replace(/"/g, """).replace(/'/g, "'")
+ .replace(//g, ">");
+}
diff --git a/context-menu-copy-link-with-types/clipboard-helper.js b/context-menu-copy-link-with-types/clipboard-helper.js
new file mode 100644
index 0000000..7e6d426
--- /dev/null
+++ b/context-menu-copy-link-with-types/clipboard-helper.js
@@ -0,0 +1,18 @@
+// This function must be called in a visible page, such as a browserAction popup
+// or a content script. Calling it in a background page has no effect!
+function copyToClipboard(text, html) {
+ function oncopy(event) {
+ document.removeEventListener("copy", oncopy, true);
+ // Hide the event from the page to prevent tampering.
+ event.stopImmediatePropagation();
+
+ // Overwrite the clipboard content.
+ event.preventDefault();
+ event.clipboardData.setData("text/plain", text);
+ event.clipboardData.setData("text/html", html);
+ }
+ document.addEventListener("copy", oncopy, true);
+
+ // Requires the clipboardWrite permission, or a user gesture:
+ document.execCommand("copy");
+}
diff --git a/context-menu-copy-link-with-types/manifest.json b/context-menu-copy-link-with-types/manifest.json
new file mode 100644
index 0000000..b01b5ba
--- /dev/null
+++ b/context-menu-copy-link-with-types/manifest.json
@@ -0,0 +1,19 @@
+{
+ "manifest_version": 2,
+ "name": "Context menu: Copy link with types",
+ "description": "Add a context menu option to links to copy the link to the clipboard, as plain text and as a link in rich HTML.",
+ "version": "1.0",
+ "homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/context-menu-copy-link-with-types",
+
+ "background": {
+ "scripts": [
+ "background.js"
+ ]
+ },
+
+ "permissions": [
+ "activeTab",
+ "contextMenus",
+ "clipboardWrite"
+ ]
+}
diff --git a/context-menu-copy-link-with-types/preview.html b/context-menu-copy-link-with-types/preview.html
new file mode 100644
index 0000000..e86dca7
--- /dev/null
+++ b/context-menu-copy-link-with-types/preview.html
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+Use Ctrl+V (or Cmd+V) to paste plain text here:
+
+
+Use Ctrl+V (or Cmd+V) to paste rich text (HTML) here:
+
+
+
+
diff --git a/context-menu-demo/README.md b/context-menu-demo/README.md
deleted file mode 100644
index 65e0426..0000000
--- a/context-menu-demo/README.md
+++ /dev/null
@@ -1,29 +0,0 @@
-# context-menu-demo
-
-A demo of the [contextMenus API](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/contextMenus/).
-
-## What it does
-
-This add-on adds several items to the browser's context menu:
-
-* one shown when there is a selection in the page, that logs the selected text
-to the browser console when clicked.
-* one shown in all contexts, that is removed when clicked.
-* two "radio" items that are shown in all contexts.
-These items are grouped using a separator item on each side.
-One radio item adds a blue border to the page, the other adds a green border.
-Note that these buttons only work on normal web pages, not special pages
-like about:debugging.
-* one "checkbox" item, shown in all contexts, whose title is updated when the
-item is clicked.
-
-## What it shows
-
-* How to create various types of context menu item:
- * normal
- * radio
- * separator
- * checkbox
-* How to use contexts to control when an item appears.
-* How to update an item's properties.
-* How to remove an item.
diff --git a/contextual-identities/README.md b/contextual-identities/README.md
new file mode 100644
index 0000000..76c435a
--- /dev/null
+++ b/contextual-identities/README.md
@@ -0,0 +1,13 @@
+# Contextual Identities
+
+## What it does
+
+Lists existing identities, lets you create new tabs with an identity and remove all tabs from an identity. For more information on contextual identities: https://wiki.mozilla.org/Security/Contextual_Identity_Project/Containers
+
+## What it shows
+
+How to use the contextualIdentities API. Please note: you must have contextualIdentities enabled. You can do that by going to about:config and setting the `privacy.userContext.enabled` preference to true. If you are using web-ext you can do this by running:
+
+web-ext run --pref privacy.userContext.enabled=true
+
+Icon from: https://www.iconfinder.com/icons/290119/card_id_identification_identity_profile_icon#size=128, License: "Free for commercial use".
diff --git a/contextual-identities/background.js b/contextual-identities/background.js
new file mode 100644
index 0000000..e69de29
diff --git a/contextual-identities/context.css b/contextual-identities/context.css
new file mode 100644
index 0000000..91381cc
--- /dev/null
+++ b/contextual-identities/context.css
@@ -0,0 +1,18 @@
+html, body {
+ width: 350px;
+}
+
+a {
+ margin: 10px;
+ display: inline-block;
+}
+
+.panel {
+ margin: 5px;
+}
+
+span.identity {
+ width: 100px;
+ display: inline-block;
+ margin-left: 1em;
+}
diff --git a/contextual-identities/context.html b/contextual-identities/context.html
new file mode 100644
index 0000000..72a823c
--- /dev/null
+++ b/contextual-identities/context.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/contextual-identities/context.js b/contextual-identities/context.js
new file mode 100644
index 0000000..8732f6e
--- /dev/null
+++ b/contextual-identities/context.js
@@ -0,0 +1,54 @@
+function eventHandler(event) {
+ if (event.target.dataset.action == 'create') {
+ browser.tabs.create({
+ url: 'about:blank',
+ cookieStoreId: event.target.dataset.identity
+ });
+ }
+ if (event.target.dataset.action == 'close-all') {
+ browser.tabs.query({
+ cookieStoreId: event.target.dataset.identity
+ }).then((tabs) => {
+ browser.tabs.remove(tabs.map((i) => i.id));
+ });
+ }
+ event.preventDefault();
+}
+
+function createOptions(node, identity) {
+ for (let option of ['Create', 'Close All']) {
+ let a = document.createElement('a');
+ a.href = '#';
+ a.innerText = option;
+ a.dataset.action = option.toLowerCase().replace(' ', '-');
+ a.dataset.identity = identity.cookieStoreId;
+ a.addEventListener('click', eventHandler);
+ node.appendChild(a);
+ }
+}
+
+var div = document.getElementById('identity-list');
+
+if (browser.contextualIdentities === undefined) {
+ div.innerText = 'browser.contextualIdentities not available. Check that the privacy.userContext.enabled pref is set to true, and reload the add-on.';
+} else {
+ browser.contextualIdentities.query({})
+ .then((identities) => {
+ if (!identities.length) {
+ div.innerText = 'No identities returned from the API.';
+ return;
+ }
+
+ for (let identity of identities) {
+ let row = document.createElement('div');
+ let span = document.createElement('span');
+ span.className = 'identity';
+ span.innerText = identity.name;
+ span.style = `color: ${identity.color}`;
+ console.log(identity);
+ row.appendChild(span);
+ createOptions(row, identity);
+ div.appendChild(row);
+ }
+ });
+}
diff --git a/contextual-identities/identity.svg b/contextual-identities/identity.svg
new file mode 100644
index 0000000..90bf628
--- /dev/null
+++ b/contextual-identities/identity.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/contextual-identities/manifest.json b/contextual-identities/manifest.json
new file mode 100644
index 0000000..cbf2ef5
--- /dev/null
+++ b/contextual-identities/manifest.json
@@ -0,0 +1,21 @@
+{
+ "browser_action": {
+ "browser_style": true,
+ "default_title": "Contextual Identities",
+ "default_popup": "context.html",
+ "default_icon": {
+ "128": "identity.svg"
+ }
+ },
+ "homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/contextual-identities",
+ "manifest_version": 2,
+ "name": "Contextual Identities",
+ "version": "1.0",
+ "permissions": [
+ "contextualIdentities",
+ "cookies"
+ ],
+ "icons": {
+ "128": "identity.svg"
+ }
+}
diff --git a/cookie-bg-picker/README.md b/cookie-bg-picker/README.md
index 5ff23e5..598f83e 100644
--- a/cookie-bg-picker/README.md
+++ b/cookie-bg-picker/README.md
@@ -5,6 +5,8 @@ The WebExtension also uses cookies to save preferences for each site you customi
Works in Firefox 47+, and will also work as a Chrome extension, out of the box.
+**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
This extension includes:
@@ -33,4 +35,4 @@ Cookie BG Picker uses the WebExtension:
## Acknowledgements
* WebExtension icon courtesy of [icons8.com](http://icons8.com).
-* Transparent background images taken from [Transparent Textures](https://www.transparenttextures.com/).
\ No newline at end of file
+* Transparent background images taken from [Transparent Textures](https://www.transparenttextures.com/).
diff --git a/cookie-bg-picker/background_scripts/background.js b/cookie-bg-picker/background_scripts/background.js
index 8a728ab..9c93b3c 100644
--- a/cookie-bg-picker/background_scripts/background.js
+++ b/cookie-bg-picker/background_scripts/background.js
@@ -1,26 +1,27 @@
/* Retrieve any previously set cookie and send to content script */
-chrome.tabs.onUpdated.addListener(cookieUpdate);
-
-function cookieUpdate(tabId, changeInfo, tab) {
- chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
- /* inject content script into current tab */
-
- chrome.tabs.executeScript(null, {
- file: "/content_scripts/updatebg.js"
- });
+function getActiveTab() {
+ return browser.tabs.query({active: true, currentWindow: true});
+}
+function cookieUpdate() {
+ getActiveTab().then((tabs) => {
// get any previously set cookie for the current tab
-
- chrome.cookies.get({
+ var gettingCookies = browser.cookies.get({
url: tabs[0].url,
name: "bgpicker"
- }, function(cookie) {
- if(cookie) {
+ });
+ gettingCookies.then((cookie) => {
+ if (cookie) {
var cookieVal = JSON.parse(cookie.value);
- chrome.tabs.sendMessage(tabs[0].id, {image: cookieVal.image});
- chrome.tabs.sendMessage(tabs[0].id, {color: cookieVal.color});
+ browser.tabs.sendMessage(tabs[0].id, {image: cookieVal.image});
+ browser.tabs.sendMessage(tabs[0].id, {color: cookieVal.color});
}
});
});
-}
\ No newline at end of file
+}
+
+// update when the tab is updated
+browser.tabs.onUpdated.addListener(cookieUpdate);
+// update when the tab is activated
+browser.tabs.onActivated.addListener(cookieUpdate);
diff --git a/cookie-bg-picker/content_scripts/updatebg.js b/cookie-bg-picker/content_scripts/updatebg.js
index 045a1e4..61479bd 100644
--- a/cookie-bg-picker/content_scripts/updatebg.js
+++ b/cookie-bg-picker/content_scripts/updatebg.js
@@ -1,13 +1,12 @@
-var html = document.querySelector('html');
-var body = document.querySelector('body');
-
-chrome.runtime.onMessage.addListener(updateBg);
+browser.runtime.onMessage.addListener(updateBg);
function updateBg(request, sender, sendResponse) {
- if(request.image) {
+ var html = document.querySelector('html');
+ var body = document.querySelector('body');
+ if (request.image) {
html.style.backgroundImage = 'url(' + request.image + ')';
body.style.backgroundImage = 'url(' + request.image + ')';
- } else if(request.color) {
+ } else if (request.color) {
html.style.backgroundColor = request.color;
body.style.backgroundColor = request.color;
} else if (request.reset) {
@@ -16,4 +15,4 @@ function updateBg(request, sender, sendResponse) {
body.style.backgroundImage = '';
body.style.backgroundColor = '';
}
-}
\ No newline at end of file
+}
diff --git a/cookie-bg-picker/manifest.json b/cookie-bg-picker/manifest.json
index 7a114ce..e626e70 100644
--- a/cookie-bg-picker/manifest.json
+++ b/cookie-bg-picker/manifest.json
@@ -9,13 +9,6 @@
"48": "icons/bgpicker-48.png"
},
- "applications": {
- "gecko": {
- "id": "quicknote@mozilla.org",
- "strict_min_version": "45.0"
- }
- },
-
"permissions": [
"tabs",
"cookies",
@@ -43,5 +36,12 @@
"background": {
"scripts": ["background_scripts/background.js"]
- }
+ },
+
+ "content_scripts": [
+ {
+ "matches": [""],
+ "js": ["content_scripts/updatebg.js"]
+ }
+ ]
}
diff --git a/cookie-bg-picker/popup/bgpicker.js b/cookie-bg-picker/popup/bgpicker.js
index af9699f..987c61f 100644
--- a/cookie-bg-picker/popup/bgpicker.js
+++ b/cookie-bg-picker/popup/bgpicker.js
@@ -6,6 +6,10 @@ var reset = document.querySelector('.color-reset button');
var cookieVal = { image : '',
color : '' };
+function getActiveTab() {
+ return browser.tabs.query({active: true, currentWindow: true});
+}
+
/* apply backgrounds to buttons */
/* add listener so that when clicked, button applies background to page HTML */
@@ -15,13 +19,13 @@ for(var i = 0; i < bgBtns.length; i++) {
bgBtns[i].style.backgroundImage = bgImg;
bgBtns[i].onclick = function(e) {
- chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
+ getActiveTab().then((tabs) => {
var imgName = e.target.getAttribute('class');
- var fullURL = chrome.extension.getURL('popup/images/'+ imgName + '.png');
- chrome.tabs.sendMessage(tabs[0].id, {image: fullURL});
+ var fullURL = browser.extension.getURL('popup/images/'+ imgName + '.png');
+ browser.tabs.sendMessage(tabs[0].id, {image: fullURL});
cookieVal.image = fullURL;
- chrome.cookies.set({
+ browser.cookies.set({
url: tabs[0].url,
name: "bgpicker",
value: JSON.stringify(cookieVal)
@@ -33,12 +37,12 @@ for(var i = 0; i < bgBtns.length; i++) {
/* apply chosen color to HTML background */
colorPick.onchange = function(e) {
- chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
+ getActiveTab().then((tabs) => {
var currColor = e.target.value;
- chrome.tabs.sendMessage(tabs[0].id, {color: currColor});
+ browser.tabs.sendMessage(tabs[0].id, {color: currColor});
cookieVal.color = currColor;
- chrome.cookies.set({
+ browser.cookies.set({
url: tabs[0].url,
name: "bgpicker",
value: JSON.stringify(cookieVal)
@@ -49,12 +53,12 @@ colorPick.onchange = function(e) {
/* reset background */
reset.onclick = function() {
- chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
- chrome.tabs.sendMessage(tabs[0].id, {reset: true});
+ getActiveTab().then((tabs) => {
+ browser.tabs.sendMessage(tabs[0].id, {reset: true});
cookieVal = { image : '',
color : '' };
- chrome.cookies.remove({
+ browser.cookies.remove({
url: tabs[0].url,
name: "bgpicker"
})
@@ -63,6 +67,9 @@ reset.onclick = function() {
/* Report cookie changes to the console */
-chrome.cookies.onChanged.addListener(function(changeInfo) {
- console.log('Cookie changed:\n* Cookie: ' + JSON.stringify(changeInfo.cookie) + '\n* Cause: ' + changeInfo.cause + '\n* Removed: ' + changeInfo.removed);
-})
\ No newline at end of file
+browser.cookies.onChanged.addListener((changeInfo) => {
+ console.log(`Cookie changed:\n
+ * Cookie: ${JSON.stringify(changeInfo.cookie)}\n
+ * Cause: ${changeInfo.cause}\n
+ * Removed: ${changeInfo.removed}`);
+});
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/discogs-search/README.md b/discogs-search/README.md
new file mode 100644
index 0000000..03a5d27
--- /dev/null
+++ b/discogs-search/README.md
@@ -0,0 +1,11 @@
+# discogs-search
+
+## What it does
+
+This add-on adds a search engine to the browser, that sends the search term to the [discogs.com](https://discogs.com) website.
+
+It also adds a keyword "disc", so you can type "disc Model 500" and get the discogs search engine without having to select it.
+
+## What it shows
+
+How to use the [`chrome_settings_overrides`](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/manifest.json/chrome_settings_overrides) manifest key to define a new search engine.
diff --git a/discogs-search/manifest.json b/discogs-search/manifest.json
new file mode 100644
index 0000000..ecaf34d
--- /dev/null
+++ b/discogs-search/manifest.json
@@ -0,0 +1,23 @@
+{
+
+ "manifest_version": 2,
+ "name": "Discogs search engine",
+ "description": "Adds a search engine that searches discogs.com",
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "55"
+ }
+ },
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "Discogs",
+ "search_url": "https://www.discogs.com/search/?q={searchTerms}",
+ "keyword": "disc",
+ "favicon_url": "https://www.discogs.com/favicon.ico",
+ "is_default": false,
+ "encoding": "UTF-8"
+ }
+ }
+
+}
diff --git a/dynamic-theme/README.md b/dynamic-theme/README.md
new file mode 100644
index 0000000..ac1d82f
--- /dev/null
+++ b/dynamic-theme/README.md
@@ -0,0 +1,17 @@
+# Dynamic theme
+
+## What it does
+
+Checks the users time and then flips between two different themes. A "moon theme" for after 8pm and before 8am and a "sun theme" the rest of the time.
+
+To flip themes you can change the system time.
+
+## What it shows
+
+How to use the dynamic theme API and the alarm API.
+
+Images are under creative commons 2.0: https://creativecommons.org/licenses/by/2.0/ and are:
+
+https://www.flickr.com/photos/smemon/21939278025/in/photolist-zqGx1e-RRJjP9-ifAAEu-5Bb3R8-RmUdfQ-do9maE-RXiUHq-avRBYY-UrLynC-NWHX1-fND353-7Hgf3N-peAT8G-ahQje-3j9m1U-7W72M7-oNgLaF-faUP9M-SCJVpm-m3zsiB-SLjW4X-6sXzow-nzmiNW-GpiC9-4yVL8D-6sYiVd-kbJFNA-dWePSs-pGoTdJ-UCKjMC-UJtDMx-UCKjf5-9iW4rf-dnhFMq-QJAu9A-VtfjzN-RLSSGN-9dWKeu-cpmnXL-4g21qm-a4t24c-edkrrt-5cQ21P-71A3Qb-qNKJoE-oQ5qc5-2f2YeN-6RGZyS-s8g3Nc-pkjybQ
+
+https://www.flickr.com/photos/carrenho/2443850489/in/photolist-4HXnBg-5Pg5tX-538XR-4kWUM-Vqb73V-GVSn1-nzSMqa-6TZwDQ-8NjEAS-ejqFwz-9Mra6z-6QDu8y-89eHW7-6fMEWp-fdabE-8Jv15V-8Jy9QG-6KcAsE-HPMGM-fcuMuE-V6vMKB-gg4VC-9h8GQa-7d1VS2-8PkWv7-8PhofV-evJBcR-MzZKt-pWWi6w-qJuTvo-dha7vm-Bnxcee-pfu11h-cUVXx1-r6BnTV-eXnhNo-h5k9E-fcT9jW-jAtbkT-5MSiEB-sfEYVj-9h8H3K-kWqQuL-8PkthY-dGFHDa-8PkWqh-o2fFjk-TWgQHJ-71rpnG-jJK6gy
diff --git a/dynamic-theme/background.js b/dynamic-theme/background.js
new file mode 100644
index 0000000..dbf6d6d
--- /dev/null
+++ b/dynamic-theme/background.js
@@ -0,0 +1,49 @@
+var currentTheme = '';
+
+const themes = {
+ 'day': {
+ images: {
+ headerURL: 'sun.jpg',
+ },
+ colors: {
+ accentcolor: '#CF723F',
+ textcolor: '#111',
+ }
+ },
+ 'night': {
+ images: {
+ headerURL: 'moon.jpg',
+ },
+ colors: {
+ accentcolor: '#000',
+ textcolor: '#fff',
+ }
+ }
+};
+
+function setTheme(theme) {
+ if (currentTheme === theme) {
+ // No point in changing the theme if it has already been set.
+ return;
+ }
+ currentTheme = theme;
+ browser.theme.update(themes[theme]);
+}
+
+function checkTime() {
+ let date = new Date();
+ let hours = date.getHours();
+ // Will set the sun theme between 8am and 8pm.
+ if ((hours > 8) && (hours < 20)) {
+ setTheme('day');
+ } else {
+ setTheme('night');
+ }
+}
+
+// On start up, check the time to see what theme to show.
+checkTime();
+
+// Set up an alarm to check this regularly.
+browser.alarms.onAlarm.addListener(checkTime);
+browser.alarms.create('checkTime', {periodInMinutes: 5});
diff --git a/dynamic-theme/manifest.json b/dynamic-theme/manifest.json
new file mode 100644
index 0000000..f98a002
--- /dev/null
+++ b/dynamic-theme/manifest.json
@@ -0,0 +1,17 @@
+{
+ "description": "An example dynamic theme",
+ "homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/dynamic-theme",
+ "manifest_version": 2,
+ "name": "Dynamic theme example",
+ "permissions": [
+ "alarms",
+ "theme"
+ ],
+ "background": {
+ "scripts": ["background.js"]
+ },
+ "version": "1.0",
+ "gecko": {
+ "strict_min_version": "55.0a2"
+ }
+}
diff --git a/dynamic-theme/moon.jpg b/dynamic-theme/moon.jpg
new file mode 100644
index 0000000..2e5d6a6
Binary files /dev/null and b/dynamic-theme/moon.jpg differ
diff --git a/dynamic-theme/sun.jpg b/dynamic-theme/sun.jpg
new file mode 100644
index 0000000..619e98c
Binary files /dev/null and b/dynamic-theme/sun.jpg differ
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-bootstrapped/README.md b/embedded-webextension-bootstrapped/README.md
new file mode 100644
index 0000000..85c5c9f
--- /dev/null
+++ b/embedded-webextension-bootstrapped/README.md
@@ -0,0 +1,14 @@
+This is an example of how to use [embedded WebExtensions](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Embedded_WebExtensions) to convert a legacy [Bootstrapped extension](https://developer.mozilla.org/en-US/Add-ons/Bootstrapped_extensions) to a [WebExtension](https://developer.mozilla.org/en-US/Add-ons/WebExtensions) in stages, and migrate the legacy add-on's data so it's accessible by the WebExtension.
+
+The legacy add-on contains:
+
+- some user data stored in the Firefox preferences
+- a button in the toolbar
+
+When the button is pressed, the add-on displays a panel containing the stored data.
+
+This directory contains three versions of the add-on.
+
+- **step0-legacy-addon**: the initial add-on, written entirely using the bootstrapped extension method.
+- **step1-hybrid-addon**: a hybrid consisting of a bootstrapped extension containing an embedded WebExtension. The bootstrapped extension reads the stored data and sends it to the embedded WebExtension. The embedded WebExtension stores the data using the [`storage`](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/storage) API, and also implements the UI.
+- **step2-pure-webextension**: the final version, written entirely using the WebExtensions method. This version can be deployed after the hybrid version has migrated the stored data to the `storage` API.
diff --git a/embedded-webextension-bootstrapped/step0-legacy-addon/bootstrap.js b/embedded-webextension-bootstrapped/step0-legacy-addon/bootstrap.js
new file mode 100644
index 0000000..364394a
--- /dev/null
+++ b/embedded-webextension-bootstrapped/step0-legacy-addon/bootstrap.js
@@ -0,0 +1,16 @@
+"use strict";
+
+function startup(data) {
+ Components.utils.import("chrome://original-bootstrap-addon-id/content/AddonPrefs.jsm");
+ Components.utils.import("chrome://original-bootstrap-addon-id/content/AddonUI.jsm");
+
+ AddonPrefs.set("super-important-user-setting", "char", "addon preference content");
+ AddonUI.init(data);
+}
+
+function shutdown(data) {
+ AddonUI.shutdown(data);
+
+ Components.utils.unload("chrome://original-bootstrap-addon-id/content/AddonUI.jsm");
+ Components.utils.unload("chrome://original-bootstrap-addon-id/content/AddonPrefs.jsm");
+}
diff --git a/embedded-webextension-bootstrapped/step0-legacy-addon/chrome.manifest b/embedded-webextension-bootstrapped/step0-legacy-addon/chrome.manifest
new file mode 100644
index 0000000..abbe1db
--- /dev/null
+++ b/embedded-webextension-bootstrapped/step0-legacy-addon/chrome.manifest
@@ -0,0 +1 @@
+content original-bootstrap-addon-id chrome/
\ No newline at end of file
diff --git a/embedded-webextension-bootstrapped/step0-legacy-addon/chrome/AddonPrefs.jsm b/embedded-webextension-bootstrapped/step0-legacy-addon/chrome/AddonPrefs.jsm
new file mode 100644
index 0000000..2d4d3cf
--- /dev/null
+++ b/embedded-webextension-bootstrapped/step0-legacy-addon/chrome/AddonPrefs.jsm
@@ -0,0 +1,41 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = ["AddonPrefs"];
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+const BASE_PREF = "extensions.original-bootstrap-addon-id.";
+
+function get(key, type = "char") {
+ key = BASE_PREF + key;
+
+ switch(type) {
+ case "char":
+ return Services.prefs.getCharPref(key);
+ case "bool":
+ return Services.prefs.getBoolPref(key);
+ case "int":
+ return Services.prefs.getIntPref(key);
+ }
+
+ throw new Error(`Unknown type: ${type}`);
+}
+
+function set(key, type, value) {
+ key = BASE_PREF + key;
+
+ switch(type) {
+ case "char":
+ return Services.prefs.setCharPref(key, value);
+ case "bool":
+ return Services.prefs.setBoolPref(key, value);
+ case "int":
+ return Services.prefs.setIntPref(key, value);
+ }
+
+ throw new Error(`Unknown type: ${type}`);
+}
+
+var AddonPrefs = {
+ get, set,
+};
diff --git a/embedded-webextension-bootstrapped/step0-legacy-addon/chrome/AddonUI.jsm b/embedded-webextension-bootstrapped/step0-legacy-addon/chrome/AddonUI.jsm
new file mode 100644
index 0000000..e014a7c
--- /dev/null
+++ b/embedded-webextension-bootstrapped/step0-legacy-addon/chrome/AddonUI.jsm
@@ -0,0 +1,65 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = ["AddonUI"];
+
+Components.utils.import("resource:///modules/CustomizableUI.jsm");
+
+Components.utils.import("chrome://original-bootstrap-addon-id/content/AddonPrefs.jsm");
+
+const BUTTON_ID = "original-bootstrap-addon-id--toolbar-button";
+const BUTTON_ICON_URL = "chrome://original-bootstrap-addon-id/content/icons/icon-32.png";
+
+const PANEL_ID = "original-bootstrap-addon-id--popup-panel";
+
+function createPanel(node) {
+ var doc = node.ownerDocument;
+
+ var panel = doc.createElement("panel");
+ panel.setAttribute("type", "arrow");
+ panel.setAttribute("id", PANEL_ID);
+ panel.setAttribute("flip", "slide");
+ panel.setAttribute("hidden", true);
+ panel.setAttribute("position", "bottomcenter topright");
+ var panelContent = doc.createElement("label");
+ panelContent.textContent = AddonPrefs.get("super-important-user-setting");
+ panel.appendChild(panelContent);
+
+ return panel;
+}
+
+function defineButtonWidget() {
+ let buttonDef = {
+ id : BUTTON_ID,
+ type : "button",
+ defaultArea : CustomizableUI.AREA_NAVBAR,
+ label : "button label",
+ tooltiptext : "button tooltip",
+ onCreated : function (node) {
+ node.setAttribute('image', BUTTON_ICON_URL);
+
+ const panel = createPanel(node);
+ node.appendChild(panel);
+
+ node.addEventListener("click", () => {
+ panel.setAttribute("hidden", false);
+ panel.openPopup(node, panel.getAttribute("position"), 0, 0, false, false);
+ });
+ }
+ };
+
+ CustomizableUI.createWidget(buttonDef);
+};
+
+
+
+function init({id}) {
+ defineButtonWidget(BUTTON_ID);
+}
+
+function shutdown({id}) {
+ CustomizableUI.destroyWidget(BUTTON_ID);
+}
+
+var AddonUI = {
+ init, shutdown,
+};
diff --git a/embedded-webextension-bootstrapped/step0-legacy-addon/chrome/icons/LICENSE b/embedded-webextension-bootstrapped/step0-legacy-addon/chrome/icons/LICENSE
new file mode 100644
index 0000000..e878a43
--- /dev/null
+++ b/embedded-webextension-bootstrapped/step0-legacy-addon/chrome/icons/LICENSE
@@ -0,0 +1 @@
+The icon "icon-32.png" is taken from the IconBeast Lite iconset, and used under the terms of its license (http://www.iconbeast.com/faq/), with a link back to the website: http://www.iconbeast.com/free/.
diff --git a/embedded-webextension-bootstrapped/step0-legacy-addon/chrome/icons/icon-32.png b/embedded-webextension-bootstrapped/step0-legacy-addon/chrome/icons/icon-32.png
new file mode 100644
index 0000000..35c2eba
Binary files /dev/null and b/embedded-webextension-bootstrapped/step0-legacy-addon/chrome/icons/icon-32.png differ
diff --git a/embedded-webextension-bootstrapped/step0-legacy-addon/install.rdf b/embedded-webextension-bootstrapped/step0-legacy-addon/install.rdf
new file mode 100644
index 0000000..71feaa4
--- /dev/null
+++ b/embedded-webextension-bootstrapped/step0-legacy-addon/install.rdf
@@ -0,0 +1,33 @@
+
+
+
+ original-bootstrap-addon-id@mozilla.com
+ 2
+ true
+ false
+ 0.1.0
+ Legacy Addon Name
+
+ A simple bootstrap addon which wants to transition to a WebExtension.
+
+ Step 0: original legacy bootstrap addon.
+
+ Luca Greco <lgreco@mozilla.com>
+
+
+
+ {ec8030f7-c20a-464f-9b0e-13a3a9e97384}
+ 49.0
+ *
+
+
+
+
+
+ {aa3c5121-dab2-40e2-81ca-7ea25febc110}
+ 49.0
+ *
+
+
+
+
diff --git a/embedded-webextension-bootstrapped/step1-hybrid-addon/bootstrap.js b/embedded-webextension-bootstrapped/step1-hybrid-addon/bootstrap.js
new file mode 100644
index 0000000..573ba93
--- /dev/null
+++ b/embedded-webextension-bootstrapped/step1-hybrid-addon/bootstrap.js
@@ -0,0 +1,24 @@
+"use strict";
+
+function startup({webExtension}) {
+ Components.utils.import("chrome://original-bootstrap-addon-id/content/AddonPrefs.jsm");
+
+ // Start the embedded webextension.
+ webExtension.startup().then(api => {
+ const {browser} = api;
+ browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
+ if (msg == "import-legacy-data") {
+ // When the embedded webextension asks for the legacy data,
+ // dump the data which needs to be preserved and send it back to the
+ // embedded extension.
+ sendReply({
+ "super-important-user-setting": AddonPrefs.get("super-important-user-setting"),
+ });
+ }
+ });
+ });
+}
+
+function shutdown(data) {
+ Components.utils.unload("chrome://original-bootstrap-addon-id/content/AddonPrefs.jsm");
+}
diff --git a/embedded-webextension-bootstrapped/step1-hybrid-addon/chrome.manifest b/embedded-webextension-bootstrapped/step1-hybrid-addon/chrome.manifest
new file mode 100644
index 0000000..abbe1db
--- /dev/null
+++ b/embedded-webextension-bootstrapped/step1-hybrid-addon/chrome.manifest
@@ -0,0 +1 @@
+content original-bootstrap-addon-id chrome/
\ No newline at end of file
diff --git a/embedded-webextension-bootstrapped/step1-hybrid-addon/chrome/AddonPrefs.jsm b/embedded-webextension-bootstrapped/step1-hybrid-addon/chrome/AddonPrefs.jsm
new file mode 100644
index 0000000..2d4d3cf
--- /dev/null
+++ b/embedded-webextension-bootstrapped/step1-hybrid-addon/chrome/AddonPrefs.jsm
@@ -0,0 +1,41 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = ["AddonPrefs"];
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+const BASE_PREF = "extensions.original-bootstrap-addon-id.";
+
+function get(key, type = "char") {
+ key = BASE_PREF + key;
+
+ switch(type) {
+ case "char":
+ return Services.prefs.getCharPref(key);
+ case "bool":
+ return Services.prefs.getBoolPref(key);
+ case "int":
+ return Services.prefs.getIntPref(key);
+ }
+
+ throw new Error(`Unknown type: ${type}`);
+}
+
+function set(key, type, value) {
+ key = BASE_PREF + key;
+
+ switch(type) {
+ case "char":
+ return Services.prefs.setCharPref(key, value);
+ case "bool":
+ return Services.prefs.setBoolPref(key, value);
+ case "int":
+ return Services.prefs.setIntPref(key, value);
+ }
+
+ throw new Error(`Unknown type: ${type}`);
+}
+
+var AddonPrefs = {
+ get, set,
+};
diff --git a/embedded-webextension-bootstrapped/step1-hybrid-addon/install.rdf b/embedded-webextension-bootstrapped/step1-hybrid-addon/install.rdf
new file mode 100644
index 0000000..c15c9b8
--- /dev/null
+++ b/embedded-webextension-bootstrapped/step1-hybrid-addon/install.rdf
@@ -0,0 +1,34 @@
+
+
+
+ original-bootstrap-addon-id@mozilla.com
+ 2
+ true
+ true
+ false
+ 0.2.0
+ Legacy Addon Name
+
+ A simple bootstrap addon which wants to transition to a WebExtension.
+
+ Step 1: transition hybrid addon.
+
+ Luca Greco <lgreco@mozilla.com>
+
+
+
+ {ec8030f7-c20a-464f-9b0e-13a3a9e97384}
+ 51.0a1
+ *
+
+
+
+
+
+ {aa3c5121-dab2-40e2-81ca-7ea25febc110}
+ 51.0a1
+ *
+
+
+
+
diff --git a/embedded-webextension-bootstrapped/step1-hybrid-addon/webextension/background.js b/embedded-webextension-bootstrapped/step1-hybrid-addon/webextension/background.js
new file mode 100644
index 0000000..b1ba3e9
--- /dev/null
+++ b/embedded-webextension-bootstrapped/step1-hybrid-addon/webextension/background.js
@@ -0,0 +1,16 @@
+"use strict";
+
+browser.storage.local.get("super-important-user-setting")
+ .then(results => {
+ // If the old preferences data has not been imported yet...
+ if (!results["super-important-user-setting"]) {
+ // Ask to the legacy part to dump the needed data and send it back
+ // to the background page...
+ browser.runtime.sendMessage("import-legacy-data").then(reply => {
+ if (reply) {
+ // Where it can be saved using the WebExtensions storage API.
+ browser.storage.local.set(reply);
+ }
+ });
+ }
+ });
diff --git a/embedded-webextension-bootstrapped/step1-hybrid-addon/webextension/icons/LICENSE b/embedded-webextension-bootstrapped/step1-hybrid-addon/webextension/icons/LICENSE
new file mode 100644
index 0000000..e878a43
--- /dev/null
+++ b/embedded-webextension-bootstrapped/step1-hybrid-addon/webextension/icons/LICENSE
@@ -0,0 +1 @@
+The icon "icon-32.png" is taken from the IconBeast Lite iconset, and used under the terms of its license (http://www.iconbeast.com/faq/), with a link back to the website: http://www.iconbeast.com/free/.
diff --git a/embedded-webextension-bootstrapped/step1-hybrid-addon/webextension/icons/icon-32.png b/embedded-webextension-bootstrapped/step1-hybrid-addon/webextension/icons/icon-32.png
new file mode 100644
index 0000000..35c2eba
Binary files /dev/null and b/embedded-webextension-bootstrapped/step1-hybrid-addon/webextension/icons/icon-32.png differ
diff --git a/embedded-webextension-bootstrapped/step1-hybrid-addon/webextension/manifest.json b/embedded-webextension-bootstrapped/step1-hybrid-addon/webextension/manifest.json
new file mode 100644
index 0000000..048c4ec
--- /dev/null
+++ b/embedded-webextension-bootstrapped/step1-hybrid-addon/webextension/manifest.json
@@ -0,0 +1,15 @@
+{
+ "name": "Legacy Addon Name",
+ "version": "0.2.0",
+ "manifest_version": 2,
+ "permissions": ["storage"],
+ "background": {
+ "scripts": ["background.js"]
+ },
+ "browser_action": {
+ "browser_style": true,
+ "default_icon": "icons/icon-32.png",
+ "default_title": "button label",
+ "default_popup": "popup.html"
+ }
+}
diff --git a/embedded-webextension-bootstrapped/step1-hybrid-addon/webextension/popup.html b/embedded-webextension-bootstrapped/step1-hybrid-addon/webextension/popup.html
new file mode 100644
index 0000000..284b7b9
--- /dev/null
+++ b/embedded-webextension-bootstrapped/step1-hybrid-addon/webextension/popup.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/embedded-webextension-bootstrapped/step1-hybrid-addon/webextension/popup.js b/embedded-webextension-bootstrapped/step1-hybrid-addon/webextension/popup.js
new file mode 100644
index 0000000..ab5360d
--- /dev/null
+++ b/embedded-webextension-bootstrapped/step1-hybrid-addon/webextension/popup.js
@@ -0,0 +1,7 @@
+"use strict";
+
+var gettingItem = browser.storage.local.get("super-important-user-setting");
+gettingItem.then(results => {
+ const panelContent = results["super-important-user-setting"] || "No settings saved.";
+ document.querySelector("#panel-content").textContent = panelContent;
+});
diff --git a/embedded-webextension-bootstrapped/step2-pure-webextension/icons/LICENSE b/embedded-webextension-bootstrapped/step2-pure-webextension/icons/LICENSE
new file mode 100644
index 0000000..e878a43
--- /dev/null
+++ b/embedded-webextension-bootstrapped/step2-pure-webextension/icons/LICENSE
@@ -0,0 +1 @@
+The icon "icon-32.png" is taken from the IconBeast Lite iconset, and used under the terms of its license (http://www.iconbeast.com/faq/), with a link back to the website: http://www.iconbeast.com/free/.
diff --git a/embedded-webextension-bootstrapped/step2-pure-webextension/icons/icon-32.png b/embedded-webextension-bootstrapped/step2-pure-webextension/icons/icon-32.png
new file mode 100644
index 0000000..35c2eba
Binary files /dev/null and b/embedded-webextension-bootstrapped/step2-pure-webextension/icons/icon-32.png differ
diff --git a/embedded-webextension-bootstrapped/step2-pure-webextension/manifest.json b/embedded-webextension-bootstrapped/step2-pure-webextension/manifest.json
new file mode 100644
index 0000000..b80f579
--- /dev/null
+++ b/embedded-webextension-bootstrapped/step2-pure-webextension/manifest.json
@@ -0,0 +1,18 @@
+{
+ "name": "Legacy Addon Name",
+ "version": "0.3.0",
+ "manifest_version": 2,
+ "permissions": ["storage"],
+ "browser_action": {
+ "browser_style": true,
+ "default_icon": "icons/icon-32.png",
+ "default_title": "button label",
+ "default_popup": "popup.html"
+ },
+ "applications": {
+ "gecko": {
+ "id": "original-bootstrap-addon-id@mozilla.com",
+ "strict_min_version": "51.0a1"
+ }
+ }
+}
diff --git a/embedded-webextension-bootstrapped/step2-pure-webextension/popup.html b/embedded-webextension-bootstrapped/step2-pure-webextension/popup.html
new file mode 100644
index 0000000..284b7b9
--- /dev/null
+++ b/embedded-webextension-bootstrapped/step2-pure-webextension/popup.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/embedded-webextension-bootstrapped/step2-pure-webextension/popup.js b/embedded-webextension-bootstrapped/step2-pure-webextension/popup.js
new file mode 100644
index 0000000..ab5360d
--- /dev/null
+++ b/embedded-webextension-bootstrapped/step2-pure-webextension/popup.js
@@ -0,0 +1,7 @@
+"use strict";
+
+var gettingItem = browser.storage.local.get("super-important-user-setting");
+gettingItem.then(results => {
+ const panelContent = results["super-important-user-setting"] || "No settings saved.";
+ document.querySelector("#panel-content").textContent = panelContent;
+});
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/embedded-webextension-sdk/README.md b/embedded-webextension-sdk/README.md
new file mode 100644
index 0000000..b72ab74
--- /dev/null
+++ b/embedded-webextension-sdk/README.md
@@ -0,0 +1,14 @@
+This is an example of how to use [embedded WebExtensions](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Embedded_WebExtensions) to convert a legacy [SDK add-on](https://developer.mozilla.org/en-US/Add-ons/SDK) to a [WebExtension](https://developer.mozilla.org/en-US/Add-ons/WebExtensions) in stages, and migrate the legacy add-on's data so it's accessible by the WebExtension.
+
+The legacy add-on contains:
+
+- A content script that is attached to any pages under "mozilla.org" or any of its subdomains. The content script sends a message to the main add-on, which then displays a [notification](https://developer.mozilla.org/en-US/Add-ons/SDK/High-Level_APIs/notifications).
+- Some user data stored using the SDK's [`simple-prefs`](https://developer.mozilla.org/en-US/Add-ons/SDK/High-Level_APIs/simple-prefs) API.
+- Some user data stored using the SDK's [`simple-storage`](https://developer.mozilla.org/en-US/Add-ons/SDK/High-Level_APIs/simple-storage) API.
+- A button in the toolbar: when the button is pressed, the add-on shows a panel containing the stored data.
+
+This directory contains three versions of the add-on.
+
+- **step0-legacy-addon**: the initial add-on, written entirely using the Add-on SDK.
+- **step1-hybrid-addon**: a hybrid consisting of an Add-on SDK add-on containing an embedded WebExtension. The Add-on SDK part sends the stored data to the embedded WebExtension. It also listens for any changes to the [`simple-prefs`](https://developer.mozilla.org/en-US/Add-ons/SDK/High-Level_APIs/simple-prefs) data, and updates the WebExtension whenever that data is changed (for example, if the user changes the data in the add-on's preferences UI under about:addons). The embedded WebExtension stores the data using the [`storage`](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/storage) API and implements everything else, including the button/panel and the content script.
+- **step2-pure-webextension**: the final version, written entirely using the WebExtensions method. This version can be deployed after the hybrid version has migrated the stored data to the `storage` API. In this version the add-on uses an [options page](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Anatomy_of_a_WebExtension#Options_pages) to provide a UI for the preferences data.
diff --git a/embedded-webextension-sdk/step0-legacy-addon/data/content-script.js b/embedded-webextension-sdk/step0-legacy-addon/data/content-script.js
new file mode 100644
index 0000000..8506fb6
--- /dev/null
+++ b/embedded-webextension-sdk/step0-legacy-addon/data/content-script.js
@@ -0,0 +1 @@
+self.port.emit("notify-attached-tab", window.location.href);
diff --git a/embedded-webextension-sdk/step0-legacy-addon/data/icons/LICENSE b/embedded-webextension-sdk/step0-legacy-addon/data/icons/LICENSE
new file mode 100644
index 0000000..e878a43
--- /dev/null
+++ b/embedded-webextension-sdk/step0-legacy-addon/data/icons/LICENSE
@@ -0,0 +1 @@
+The icon "icon-32.png" is taken from the IconBeast Lite iconset, and used under the terms of its license (http://www.iconbeast.com/faq/), with a link back to the website: http://www.iconbeast.com/free/.
diff --git a/embedded-webextension-sdk/step0-legacy-addon/data/icons/icon-32.png b/embedded-webextension-sdk/step0-legacy-addon/data/icons/icon-32.png
new file mode 100644
index 0000000..35c2eba
Binary files /dev/null and b/embedded-webextension-sdk/step0-legacy-addon/data/icons/icon-32.png differ
diff --git a/embedded-webextension-sdk/step0-legacy-addon/data/popup.html b/embedded-webextension-sdk/step0-legacy-addon/data/popup.html
new file mode 100644
index 0000000..9f208a9
--- /dev/null
+++ b/embedded-webextension-sdk/step0-legacy-addon/data/popup.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/embedded-webextension-sdk/step0-legacy-addon/data/popup.js b/embedded-webextension-sdk/step0-legacy-addon/data/popup.js
new file mode 100644
index 0000000..fc69eec
--- /dev/null
+++ b/embedded-webextension-sdk/step0-legacy-addon/data/popup.js
@@ -0,0 +1,3 @@
+addon.port.on("got-user-data", results => {
+ document.querySelector("#panel-content").textContent = JSON.stringify(results, null, 2);
+});
diff --git a/embedded-webextension-sdk/step0-legacy-addon/lib/addon-ui.js b/embedded-webextension-sdk/step0-legacy-addon/lib/addon-ui.js
new file mode 100644
index 0000000..022206e
--- /dev/null
+++ b/embedded-webextension-sdk/step0-legacy-addon/lib/addon-ui.js
@@ -0,0 +1,42 @@
+const { ToggleButton } = require('sdk/ui/button/toggle');
+const panels = require("sdk/panel");
+const self = require("sdk/self");
+const ss = require("sdk/simple-storage");
+const sp = require("sdk/simple-prefs");
+
+const button = ToggleButton({
+ id: "my-button",
+ label: "my button",
+ icon: {
+ "32": self.data.url("icons/icon-32.png"),
+ },
+ onChange: handleChange,
+});
+
+const panel = panels.Panel({
+ contentURL: self.data.url("popup.html"),
+ onHide: handleHide,
+});
+
+panel.on("show", () => {
+ panel.port.emit("got-user-data", {
+ prefs: {
+ superImportantUserPref: sp.prefs["superImportantUserPref"],
+ },
+ storage: {
+ superImportantUserStoredData: ss.storage.superImportantUserStoredData,
+ },
+ });
+});
+
+function handleChange(state) {
+ if (state.checked) {
+ panel.show({
+ position: button,
+ });
+ }
+}
+
+function handleHide() {
+ button.state('window', {checked: false});
+}
diff --git a/embedded-webextension-sdk/step0-legacy-addon/lib/content-scripts.js b/embedded-webextension-sdk/step0-legacy-addon/lib/content-scripts.js
new file mode 100644
index 0000000..cf4f1fa
--- /dev/null
+++ b/embedded-webextension-sdk/step0-legacy-addon/lib/content-scripts.js
@@ -0,0 +1,18 @@
+const data = require("sdk/self").data;
+const pageMod = require("sdk/page-mod");
+const notifications = require("sdk/notifications");
+
+pageMod.PageMod({
+ include: "*.mozilla.org",
+ contentScriptFile: [
+ data.url("content-script.js"),
+ ],
+ onAttach: function(worker) {
+ worker.port.on("notify-attached-tab", (msg) => {
+ notifications.notify({
+ title: "Attached to tab",
+ text: msg
+ });
+ });
+ }
+});
diff --git a/embedded-webextension-sdk/step0-legacy-addon/lib/user-data-storage.js b/embedded-webextension-sdk/step0-legacy-addon/lib/user-data-storage.js
new file mode 100644
index 0000000..d9abd8d
--- /dev/null
+++ b/embedded-webextension-sdk/step0-legacy-addon/lib/user-data-storage.js
@@ -0,0 +1,3 @@
+const ss = require("sdk/simple-storage");
+
+ss.storage.superImportantUserStoredData = "This value was saved in the simple-storage";
diff --git a/embedded-webextension-sdk/step0-legacy-addon/main.js b/embedded-webextension-sdk/step0-legacy-addon/main.js
new file mode 100644
index 0000000..ced1b02
--- /dev/null
+++ b/embedded-webextension-sdk/step0-legacy-addon/main.js
@@ -0,0 +1,3 @@
+require("./lib/addon-ui");
+require("./lib/user-data-storage");
+require("./lib/content-scripts");
diff --git a/embedded-webextension-sdk/step0-legacy-addon/package.json b/embedded-webextension-sdk/step0-legacy-addon/package.json
new file mode 100644
index 0000000..92d1c8d
--- /dev/null
+++ b/embedded-webextension-sdk/step0-legacy-addon/package.json
@@ -0,0 +1,20 @@
+{
+ "id": "original-sdk-addon-id@mozilla.com",
+ "version": "0.1.0",
+ "main": "./main.js",
+ "name": "sdk-addon-name",
+ "fullName": "SDK Addon Name",
+ "description": "A simple SDK addon which wants to transition to a WebExtension",
+ "preferences": [
+ {
+ "name": "superImportantUserPref",
+ "title": "Super important user preference",
+ "type": "string",
+ "value": "saved superImportantUserPref value"
+ }
+ ],
+ "engines": {
+ "firefox": ">= 49",
+ "fennec": ">= 49"
+ }
+}
diff --git a/embedded-webextension-sdk/step1-hybrid-addon/lib/user-data-storage.js b/embedded-webextension-sdk/step1-hybrid-addon/lib/user-data-storage.js
new file mode 100644
index 0000000..ad616b3
--- /dev/null
+++ b/embedded-webextension-sdk/step1-hybrid-addon/lib/user-data-storage.js
@@ -0,0 +1,25 @@
+const sp = require("sdk/simple-prefs");
+const ss = require("sdk/simple-storage");
+
+ss.storage.superImportantUserStoredData = "This value was saved in the simple-storage";
+
+exports.setSyncLegacyDataPort = function(port) {
+ // Send the initial data dump.
+ port.postMessage({
+ prefs: {
+ superImportantUserPref: sp.prefs["superImportantUserPref"],
+ },
+ storage: {
+ superImportantUserStoredData: ss.storage.superImportantUserStoredData,
+ },
+ });
+
+ // Keep the preferences in sync with the data stored in the webextension.
+ sp.on("superImportantUserPref", () => {
+ port.postMessage({
+ prefs: {
+ superImportantUserPref: sp.prefs["superImportantUserPref"],
+ }
+ });
+ });
+};
diff --git a/embedded-webextension-sdk/step1-hybrid-addon/main.js b/embedded-webextension-sdk/step1-hybrid-addon/main.js
new file mode 100644
index 0000000..5c02fce
--- /dev/null
+++ b/embedded-webextension-sdk/step1-hybrid-addon/main.js
@@ -0,0 +1,10 @@
+const webext = require("sdk/webextension");
+const {setSyncLegacyDataPort} = require("./lib/user-data-storage");
+
+webext.startup().then(({browser}) => {
+ browser.runtime.onConnect.addListener(port => {
+ if (port.name === "sync-legacy-addon-data") {
+ setSyncLegacyDataPort(port);
+ }
+ });
+});
diff --git a/embedded-webextension-sdk/step1-hybrid-addon/package.json b/embedded-webextension-sdk/step1-hybrid-addon/package.json
new file mode 100644
index 0000000..62615b0
--- /dev/null
+++ b/embedded-webextension-sdk/step1-hybrid-addon/package.json
@@ -0,0 +1,21 @@
+{
+ "id": "original-sdk-addon-id@mozilla.com",
+ "version": "0.2.0",
+ "main": "./main.js",
+ "name": "sdk-addon-name",
+ "fullName": "SDK Addon Name",
+ "description": "A simple SDK addon which wants to transition to a WebExtension",
+ "preferences": [
+ {
+ "name": "superImportantUserPref",
+ "title": "Super important user preference",
+ "type": "string",
+ "value": "saved superImportantUserPref value"
+ }
+ ],
+ "engines": {
+ "firefox": ">= 51.0a1",
+ "fennec": ">= 51.0a1"
+ },
+ "hasEmbeddedWebExtension": true
+}
diff --git a/embedded-webextension-sdk/step1-hybrid-addon/webextension/background.js b/embedded-webextension-sdk/step1-hybrid-addon/webextension/background.js
new file mode 100644
index 0000000..9f9a472
--- /dev/null
+++ b/embedded-webextension-sdk/step1-hybrid-addon/webextension/background.js
@@ -0,0 +1,25 @@
+"use strict";
+
+// Ask to the legacy part to dump the needed data and send it back
+// to the background page...
+var port = browser.runtime.connect({name: "sync-legacy-addon-data"});
+port.onMessage.addListener((msg) => {
+ if (msg) {
+ // Where it can be saved using the WebExtensions storage API.
+ browser.storage.local.set(msg);
+ }
+});
+
+browser.runtime.onMessage.addListener(msg => {
+ const {type} = msg;
+
+ switch (type) {
+ case "notify-attached-tab":
+ browser.notifications.create({
+ type: "basic",
+ title: "Attached to tab",
+ message: msg.message
+ });
+ break;
+ }
+});
diff --git a/embedded-webextension-sdk/step1-hybrid-addon/webextension/content-script.js b/embedded-webextension-sdk/step1-hybrid-addon/webextension/content-script.js
new file mode 100644
index 0000000..80a9dae
--- /dev/null
+++ b/embedded-webextension-sdk/step1-hybrid-addon/webextension/content-script.js
@@ -0,0 +1,4 @@
+browser.runtime.sendMessage({
+ type: "notify-attached-tab",
+ message: window.location.href,
+});
diff --git a/embedded-webextension-sdk/step1-hybrid-addon/webextension/icons/LICENSE b/embedded-webextension-sdk/step1-hybrid-addon/webextension/icons/LICENSE
new file mode 100644
index 0000000..e878a43
--- /dev/null
+++ b/embedded-webextension-sdk/step1-hybrid-addon/webextension/icons/LICENSE
@@ -0,0 +1 @@
+The icon "icon-32.png" is taken from the IconBeast Lite iconset, and used under the terms of its license (http://www.iconbeast.com/faq/), with a link back to the website: http://www.iconbeast.com/free/.
diff --git a/embedded-webextension-sdk/step1-hybrid-addon/webextension/icons/icon-32.png b/embedded-webextension-sdk/step1-hybrid-addon/webextension/icons/icon-32.png
new file mode 100644
index 0000000..35c2eba
Binary files /dev/null and b/embedded-webextension-sdk/step1-hybrid-addon/webextension/icons/icon-32.png differ
diff --git a/embedded-webextension-sdk/step1-hybrid-addon/webextension/manifest.json b/embedded-webextension-sdk/step1-hybrid-addon/webextension/manifest.json
new file mode 100644
index 0000000..0c4b37f
--- /dev/null
+++ b/embedded-webextension-sdk/step1-hybrid-addon/webextension/manifest.json
@@ -0,0 +1,21 @@
+{
+ "name": "SDK Transition Addon",
+ "version": "0.2.0",
+ "manifest_version": 2,
+ "permissions": ["storage", "notifications"],
+ "background": {
+ "scripts": ["background.js"]
+ },
+ "content_scripts": [
+ {
+ "matches": ["*://*.mozilla.org/*"],
+ "js": ["content-script.js"]
+ }
+ ],
+ "browser_action": {
+ "browser_style": true,
+ "default_icon": "icons/icon-32.png",
+ "default_title": "button label",
+ "default_popup": "popup.html"
+ }
+}
diff --git a/embedded-webextension-sdk/step1-hybrid-addon/webextension/popup.html b/embedded-webextension-sdk/step1-hybrid-addon/webextension/popup.html
new file mode 100644
index 0000000..9f208a9
--- /dev/null
+++ b/embedded-webextension-sdk/step1-hybrid-addon/webextension/popup.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/embedded-webextension-sdk/step1-hybrid-addon/webextension/popup.js b/embedded-webextension-sdk/step1-hybrid-addon/webextension/popup.js
new file mode 100644
index 0000000..a58ece8
--- /dev/null
+++ b/embedded-webextension-sdk/step1-hybrid-addon/webextension/popup.js
@@ -0,0 +1,4 @@
+const gettingItem = browser.storage.local.get();
+gettingItem.then((results) => {
+ document.querySelector("#panel-content").textContent = JSON.stringify(results, null, 2);
+});
diff --git a/embedded-webextension-sdk/step2-pure-webextension/background.js b/embedded-webextension-sdk/step2-pure-webextension/background.js
new file mode 100644
index 0000000..bf59a60
--- /dev/null
+++ b/embedded-webextension-sdk/step2-pure-webextension/background.js
@@ -0,0 +1,15 @@
+"use strict";
+
+browser.runtime.onMessage.addListener(msg => {
+ const {type} = msg;
+
+ switch (type) {
+ case "notify-attached-tab":
+ browser.notifications.create({
+ type: "basic",
+ title: "Attached to tab",
+ message: msg.message
+ });
+ break;
+ }
+});
diff --git a/embedded-webextension-sdk/step2-pure-webextension/content-script.js b/embedded-webextension-sdk/step2-pure-webextension/content-script.js
new file mode 100644
index 0000000..80a9dae
--- /dev/null
+++ b/embedded-webextension-sdk/step2-pure-webextension/content-script.js
@@ -0,0 +1,4 @@
+browser.runtime.sendMessage({
+ type: "notify-attached-tab",
+ message: window.location.href,
+});
diff --git a/embedded-webextension-sdk/step2-pure-webextension/icons/LICENSE b/embedded-webextension-sdk/step2-pure-webextension/icons/LICENSE
new file mode 100644
index 0000000..e878a43
--- /dev/null
+++ b/embedded-webextension-sdk/step2-pure-webextension/icons/LICENSE
@@ -0,0 +1 @@
+The icon "icon-32.png" is taken from the IconBeast Lite iconset, and used under the terms of its license (http://www.iconbeast.com/faq/), with a link back to the website: http://www.iconbeast.com/free/.
diff --git a/embedded-webextension-sdk/step2-pure-webextension/icons/icon-32.png b/embedded-webextension-sdk/step2-pure-webextension/icons/icon-32.png
new file mode 100644
index 0000000..35c2eba
Binary files /dev/null and b/embedded-webextension-sdk/step2-pure-webextension/icons/icon-32.png differ
diff --git a/embedded-webextension-sdk/step2-pure-webextension/manifest.json b/embedded-webextension-sdk/step2-pure-webextension/manifest.json
new file mode 100644
index 0000000..ba06037
--- /dev/null
+++ b/embedded-webextension-sdk/step2-pure-webextension/manifest.json
@@ -0,0 +1,30 @@
+{
+ "name": "SDK Addon Name",
+ "version": "0.3.0",
+ "manifest_version": 2,
+ "permissions": ["storage", "notifications"],
+ "background": {
+ "scripts": ["background.js"]
+ },
+ "content_scripts": [
+ {
+ "matches": ["*://*.mozilla.org/*"],
+ "js": ["content-script.js"]
+ }
+ ],
+ "options_ui": {
+ "page": "options.html"
+ },
+ "browser_action": {
+ "browser_style": true,
+ "default_icon": "icons/icon-32.png",
+ "default_title": "button label",
+ "default_popup": "popup.html"
+ },
+ "applications": {
+ "gecko": {
+ "id": "original-sdk-addon-id@mozilla.com",
+ "strict_min_version": "51.0a1"
+ }
+ }
+}
diff --git a/embedded-webextension-sdk/step2-pure-webextension/options.html b/embedded-webextension-sdk/step2-pure-webextension/options.html
new file mode 100644
index 0000000..a2deba8
--- /dev/null
+++ b/embedded-webextension-sdk/step2-pure-webextension/options.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/embedded-webextension-sdk/step2-pure-webextension/options.js b/embedded-webextension-sdk/step2-pure-webextension/options.js
new file mode 100644
index 0000000..22a840e
--- /dev/null
+++ b/embedded-webextension-sdk/step2-pure-webextension/options.js
@@ -0,0 +1,21 @@
+const gettingItem = browser.storage.local.get("prefs");
+gettingItem.then(results => {
+ const {prefs} = results || {
+ prefs: {
+ superImportantUserPref: "default value"
+ },
+ };
+
+ const el = document.querySelector("#superImportantUserPref");
+ el.value = prefs.superImportantUserPref;
+
+ const updatePref = () => {
+ browser.storage.local.set({
+ prefs: {
+ superImportantUserPref: el.value,
+ },
+ });
+ };
+
+ el.addEventListener("input", updatePref);
+});
diff --git a/embedded-webextension-sdk/step2-pure-webextension/popup.html b/embedded-webextension-sdk/step2-pure-webextension/popup.html
new file mode 100644
index 0000000..9f208a9
--- /dev/null
+++ b/embedded-webextension-sdk/step2-pure-webextension/popup.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/embedded-webextension-sdk/step2-pure-webextension/popup.js b/embedded-webextension-sdk/step2-pure-webextension/popup.js
new file mode 100644
index 0000000..a58ece8
--- /dev/null
+++ b/embedded-webextension-sdk/step2-pure-webextension/popup.js
@@ -0,0 +1,4 @@
+const gettingItem = browser.storage.local.get();
+gettingItem.then((results) => {
+ document.querySelector("#panel-content").textContent = JSON.stringify(results, null, 2);
+});
diff --git a/emoji-substitution/README.md b/emoji-substitution/README.md
new file mode 100644
index 0000000..e29b7f2
--- /dev/null
+++ b/emoji-substitution/README.md
@@ -0,0 +1,11 @@
+# Emoji Substitution
+
+**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
+
+Replaces words that describe an emoji with the emoji itself. This runs as a content script and scans web pages, looking for text that can be replaced with emoji. As an example, after installing visit https://mozilla.org and notice that the text "firefox" should change.
+
+## What it shows
+
+A good example for beginners that can be used as a "make your first add-on" tutorial and / or referenced to create other add-ons.
diff --git a/emoji-substitution/emojiMap.js b/emoji-substitution/emojiMap.js
new file mode 100644
index 0000000..b2f1b9e
--- /dev/null
+++ b/emoji-substitution/emojiMap.js
@@ -0,0 +1,118 @@
+/*
+ * This file contains the Map of word --> emoji substitutions.
+ */
+
+/* exported sortedEmojiMap */
+
+let dictionary = new Map();
+dictionary.set('apple', '🍎');
+dictionary.set('banana', '🍌');
+dictionary.set('bang', '💥');
+dictionary.set('baseball', '⚾');
+dictionary.set('basketball', '🏀');
+dictionary.set('beer', '🍺');
+dictionary.set('bicycle', '🚴');
+dictionary.set('bike', '🚴');
+dictionary.set('bomb', '💣');
+dictionary.set('boy', '👦');
+dictionary.set('bug', '🐛');
+dictionary.set('burger', '🍔');
+dictionary.set('burn', '🔥');
+dictionary.set('cake', '🎂');
+dictionary.set('candy', '🍬');
+dictionary.set('cat', '🐱');
+dictionary.set('celebration', '🎉');
+dictionary.set('cheeseburger', '🍔');
+dictionary.set('cookie', '🍪');
+dictionary.set('cool', '😎');
+dictionary.set('cry', '😢');
+dictionary.set('dog', '🐶');
+dictionary.set('doge', '🐕');
+dictionary.set('earth', '🌎');
+dictionary.set('explode', '💥');
+dictionary.set('fart', '💨');
+dictionary.set('fast', '💨');
+dictionary.set('female', '👩');
+dictionary.set('fire', '🔥');
+dictionary.set('fish', '🐟');
+dictionary.set('flame', '🔥');
+dictionary.set('flower', '🌹');
+dictionary.set('food', '🍕');
+dictionary.set('football', '🏈');
+dictionary.set('girl', '👧');
+dictionary.set('golf', '⛳');
+dictionary.set('hamburger', '🍔');
+dictionary.set('happy', '😀');
+dictionary.set('horse', '🐴');
+dictionary.set('hot', '🔥');
+dictionary.set('kiss', '😘');
+dictionary.set('laugh', '😂');
+dictionary.set('lit', '🔥');
+dictionary.set('lock', '🔒');
+dictionary.set('lol', '😂');
+dictionary.set('love', '😍');
+dictionary.set('male', '👨');
+dictionary.set('man', '👨');
+dictionary.set('monkey', '🐵');
+dictionary.set('moon', '🌙');
+dictionary.set('note', '📝');
+dictionary.set('paint', '🎨');
+dictionary.set('panda', '🐼');
+dictionary.set('party', '🎉');
+dictionary.set('pig', '🐷');
+dictionary.set('pizza', '🍕');
+dictionary.set('planet', '🌎');
+dictionary.set('rose', '🌹');
+dictionary.set('rofl', '😂');
+dictionary.set('sad', '😢');
+dictionary.set('sleep', '😴');
+dictionary.set('smile', '😀');
+dictionary.set('smiley', '😀');
+dictionary.set('soccer', '⚽');
+dictionary.set('star', '⭐');
+dictionary.set('sun', '☀️');
+dictionary.set('sunglasses', '😎');
+dictionary.set('surprised', '😮');
+dictionary.set('tree', '🌲');
+dictionary.set('trophy', '🏆');
+dictionary.set('win', '🏆');
+dictionary.set('wind', '💨');
+dictionary.set('wine', '🍷');
+dictionary.set('wink', '😉');
+dictionary.set('woman', '👩');
+dictionary.set('world', '🌎');
+dictionary.set('wow', '😮');
+
+/*
+ * After all the dictionary entries have been set, sort them by length.
+ *
+ * Because iteration over Maps happens by insertion order, this avoids
+ * scenarios where words that are substrings of other words get substituted
+ * first, leading to the longer word's substitution never triggering.
+ *
+ * For example, the 'woman' substitution would never get triggered
+ * if the 'man' substitution happens first because the input term 'woman'
+ * would become 'wo👨', and the search for 'woman' would not find any matches.
+ */
+let tempArray = Array.from(dictionary);
+tempArray.sort((pair1, pair2) => {
+ // Each pair is an array with two entries: a word, and its emoji.
+ // Ex: ['woman', '👩']
+ const firstWord = pair1[0];
+ const secondWord = pair2[0];
+
+ if (firstWord.length > secondWord.length) {
+ // The first word should come before the second word.
+ return -1;
+ }
+ if (secondWord.length > firstWord.length) {
+ // The second word should come before the first word.
+ return 1;
+ }
+
+ // The words have the same length, it doesn't matter which comes first.
+ return 0;
+});
+
+// Now that the entries are sorted, put them back into a Map.
+let sortedEmojiMap = new Map(tempArray);
diff --git a/emoji-substitution/icons/icon.png b/emoji-substitution/icons/icon.png
new file mode 100644
index 0000000..b35102a
Binary files /dev/null and b/emoji-substitution/icons/icon.png differ
diff --git a/emoji-substitution/icons/icon@2x.png b/emoji-substitution/icons/icon@2x.png
new file mode 100644
index 0000000..a5acdb1
Binary files /dev/null and b/emoji-substitution/icons/icon@2x.png differ
diff --git a/emoji-substitution/manifest.json b/emoji-substitution/manifest.json
new file mode 100644
index 0000000..e6f1067
--- /dev/null
+++ b/emoji-substitution/manifest.json
@@ -0,0 +1,18 @@
+{
+ "manifest_version": 2,
+ "name": "Emoji Substitution",
+ "description": "Replaces words with emojis.",
+ "homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/emoji-substitution",
+ "version": "1.0",
+ "icons": {
+ "48": "icons/icon.png",
+ "96": "icons/icon@2x.png"
+ },
+
+ "content_scripts": [
+ {
+ "matches": [""],
+ "js": ["./emojiMap.js", "./substitute.js"]
+ }
+ ]
+}
diff --git a/emoji-substitution/substitute.js b/emoji-substitution/substitute.js
new file mode 100644
index 0000000..39203db
--- /dev/null
+++ b/emoji-substitution/substitute.js
@@ -0,0 +1,95 @@
+/*
+ * This file is responsible for performing the logic of replacing
+ * 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;
+
+/*
+ * For efficiency, create a word --> search RegEx Map too.
+ */
+let regexs = new Map();
+for (let word of emojiMap.keys()) {
+ // We want a global, case-insensitive replacement.
+ // @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp
+ regexs.set(word, new RegExp(word, 'gi'));
+}
+
+/**
+ * Substitutes emojis into text nodes.
+ * If the node contains more than just text (ex: it has child nodes),
+ * call replaceText() on each of its children.
+ *
+ * @param {Node} node - The target DOM Node.
+ * @return {void} - Note: the emoji substitution is done inline.
+ */
+function replaceText (node) {
+ // Setting textContent on a node removes all of its children and replaces
+ // them with a single text node. Since we don't want to alter the DOM aside
+ // from substituting text, we only substitute on single text nodes.
+ // @see https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent
+ if (node.nodeType === Node.TEXT_NODE) {
+ // This node only contains text.
+ // @see https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType.
+
+ // Skip textarea nodes due to the potential for accidental submission
+ // of substituted emoji where none was intended.
+ if (node.parentNode &&
+ node.parentNode.nodeName === 'TEXTAREA') {
+ return;
+ }
+
+ // Because DOM manipulation is slow, we don't want to keep setting
+ // textContent after every replacement. Instead, manipulate a copy of
+ // this string outside of the DOM and then perform the manipulation
+ // once, at the end.
+ let content = node.textContent;
+
+ // Replace every occurrence of 'word' in 'content' with its emoji.
+ // Use the emojiMap for replacements.
+ for (let [word, emoji] of emojiMap) {
+ // Grab the search regex for this word.
+ const regex = regexs.get(word);
+
+ // Actually do the replacement / substitution.
+ // Note: if 'word' does not appear in 'content', nothing happens.
+ content = content.replace(regex, emoji);
+ }
+
+ // Now that all the replacements are done, perform the DOM manipulation.
+ node.textContent = content;
+ }
+ else {
+ // This node contains more than just text, call replaceText() on each
+ // of its children.
+ for (let i = 0; i < node.childNodes.length; i++) {
+ replaceText(node.childNodes[i]);
+ }
+ }
+}
+
+// Start the recursion from the body tag.
+replaceText(document.body);
+
+// Now monitor the DOM for additions and substitute emoji into new nodes.
+// @see https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver.
+const observer = new MutationObserver((mutations) => {
+ mutations.forEach((mutation) => {
+ if (mutation.addedNodes && mutation.addedNodes.length > 0) {
+ // This DOM change was new nodes being added. Run our substitution
+ // algorithm on each newly added node.
+ for (let i = 0; i < mutation.addedNodes.length; i++) {
+ const newNode = mutation.addedNodes[i];
+ replaceText(newNode);
+ }
+ }
+ });
+});
+observer.observe(document.body, {
+ childList: true,
+ subtree: true
+});
diff --git a/eslint-example/.eslintrc.json b/eslint-example/.eslintrc.json
new file mode 100644
index 0000000..76c54fb
--- /dev/null
+++ b/eslint-example/.eslintrc.json
@@ -0,0 +1,10 @@
+{
+ "env": {
+ "browser": true,
+ "es6": true,
+ "webextensions": true
+ },
+ "extends": [
+ "eslint:recommended"
+ ]
+}
diff --git a/eslint-example/.gitignore b/eslint-example/.gitignore
new file mode 100644
index 0000000..c2658d7
--- /dev/null
+++ b/eslint-example/.gitignore
@@ -0,0 +1 @@
+node_modules/
diff --git a/eslint-example/README.md b/eslint-example/README.md
new file mode 100644
index 0000000..ca380ba
--- /dev/null
+++ b/eslint-example/README.md
@@ -0,0 +1,17 @@
+# ESLint Example
+
+## What it shows
+
+This shows how to configure a WebExtension with
+[eslint](http://eslint.org/)
+to protect against
+writing JavaScript code that may be incompatible with modern versions of
+Firefox or Chrome.
+
+## How to use it
+
+This requires [NodeJS](https://nodejs.org/en/) and [npm](http://npmjs.com/).
+
+* Change into the example directory and run `npm install` to install all
+ dependencies.
+* Execute `npm run lint` to view a report of any coding errors.
diff --git a/eslint-example/file.js b/eslint-example/file.js
new file mode 100644
index 0000000..62f4e6d
--- /dev/null
+++ b/eslint-example/file.js
@@ -0,0 +1,7 @@
+// This special eslint comment will declare that the named
+// function has been "exported" into the global scope.
+
+/* exported getUsefulContents */
+function getUsefulContents(callback) {
+ callback('Hello World');
+}
diff --git a/context-menu-demo/icons/LICENSE b/eslint-example/icons/LICENSE
similarity index 100%
rename from context-menu-demo/icons/LICENSE
rename to eslint-example/icons/LICENSE
diff --git a/context-menu-demo/icons/page-32.png b/eslint-example/icons/page-32.png
similarity index 100%
rename from context-menu-demo/icons/page-32.png
rename to eslint-example/icons/page-32.png
diff --git a/context-menu-demo/icons/page-48.png b/eslint-example/icons/page-48.png
similarity index 100%
rename from context-menu-demo/icons/page-48.png
rename to eslint-example/icons/page-48.png
diff --git a/eslint-example/main.js b/eslint-example/main.js
new file mode 100644
index 0000000..5f5981e
--- /dev/null
+++ b/eslint-example/main.js
@@ -0,0 +1,13 @@
+// This special eslint comment declares that the code below relies on
+// a named function in the global scope.
+
+/* global getUsefulContents */
+function start() {
+ getUsefulContents(data => {
+ var display = document.getElementById('display');
+
+ display.innerHTML = data;
+ });
+}
+
+document.addEventListener('DOMContentLoaded', start);
diff --git a/eslint-example/manifest.json b/eslint-example/manifest.json
new file mode 100644
index 0000000..21dfebf
--- /dev/null
+++ b/eslint-example/manifest.json
@@ -0,0 +1,12 @@
+{
+ "manifest_version": 2,
+ "description": "Example using eslint",
+ "name": "eslint-example",
+ "version": "1.0",
+ "homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/eslint-example",
+
+ "browser_action": {
+ "default_icon": "icons/page-32.png",
+ "default_popup": "popup.html"
+ }
+}
\ No newline at end of file
diff --git a/eslint-example/package.json b/eslint-example/package.json
new file mode 100644
index 0000000..72cfd78
--- /dev/null
+++ b/eslint-example/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "eslint-example",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "devDependencies": {
+ "eslint": "^3.19.0"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "lint": "eslint ."
+ },
+ "author": "",
+ "license": "ISC"
+}
diff --git a/eslint-example/popup.html b/eslint-example/popup.html
new file mode 100644
index 0000000..92abfaa
--- /dev/null
+++ b/eslint-example/popup.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Pop-up
+
+
+
Example.com
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples.json b/examples.json
index de710f7..31ad03a 100644
--- a/examples.json
+++ b/examples.json
@@ -1,477 +1,419 @@
-
[
- {
- "name": "beastify",
- "description": "Adds a browser action icon to the toolbar. Click the button to choose a beast. The active tab's body content is then replaced with a picture of the chosen beast.",
- "url": "https://github.com/mdn/webextensions-examples/tree/master/beastify",
- "manifest_keys": [
- "permissions",
- "browser_action",
- "web_accessible_resources"
- ],
- "javascript_modules": [
- {
- "name": "tabs",
- "apis": [
- "executeScript",
- "sendMessage",
- "query"
- ]
- },
- {
- "name": "extension",
- "apis": [
- "getURL"
- ]
- },
- {
- "name": "runtime",
- "apis": [
- "onMessage"
- ]
- }
- ]
- },
- {
- "name": "Bookmark it!",
- "description": "A simple bookmark button",
- "url": "https://github.com/mdn/webextensions-examples/tree/master/bookmark-it",
- "manifest_keys": [
- "permissions",
- "browser_action",
- "background"
- ],
- "javascript_modules": [
- {
- "name": "bookmarks",
- "apis": [
- "remove",
- "create",
- "search"
- ]
- },
- {
- "name": "browserAction",
- "apis": [
- "setIcon",
- "onClicked"
- ]
- },
- {
- "name": "tabs",
- "apis": [
- "query",
- "onUpdated",
- "onActivated"
- ]
- }
- ]
- },
- {
- "name": "borderify",
- "description": "Adds a solid red border to all webpages matching mozilla.org.",
- "url": "https://github.com/mdn/webextensions-examples/tree/master/borderify",
- "manifest_keys": [
- "content_scripts"
- ],
- "javascript_modules": []
- },
- {
- "name": "chill-out",
- "description": "Show a page action after a period of inactivity. Show cat gifs when the page action is clicked.",
- "url": "https://github.com/mdn/webextensions-examples/tree/master/chill-out",
- "manifest_keys": [
- "permissions",
- "page_action",
- "background"
- ],
- "javascript_modules": [
- {
- "name": "alarms",
- "apis": [
- "onAlarm",
- "clearAll",
- "create"
- ]
- },
- {
- "name": "pageAction",
- "apis": [
- "show",
- "hide",
- "onClicked"
- ]
- },
- {
- "name": "tabs",
- "apis": [
- "update",
- "query",
- "onUpdated",
- "onActivated",
- "get"
- ]
- }
- ]
- },
- {
- "name": "commands",
- "description": "Press Ctrl+Shift+Y to send an event (Command+Shift+Y on a Mac).",
- "url": "https://github.com/mdn/webextensions-examples/tree/master/commands",
- "manifest_keys": [
- "commands",
- "background"
- ],
- "javascript_modules": [
- {
+ {
+ "javascript_apis": [
+ "storage.local",
+ "tabs.onActivated",
+ "tabs.onUpdated",
+ "tabs.query",
+ "windows.getCurrent"
+ ],
+ "name": "annotate-page",
+ "description": "Displays a sidebar that lets you take notes on web pages."
+ },
+ {
+ "javascript_apis": [
+ "pageAction.getTitle",
+ "pageAction.onClicked",
+ "pageAction.setIcon",
+ "pageAction.setTitle",
+ "pageAction.show",
+ "tabs.insertCSS",
+ "tabs.onUpdated",
+ "tabs.query",
+ "tabs.removeCSS"
+ ],
+ "name": "apply-css",
+ "description": "Adds a page action to the toolbar. Click the button to apply a red border using injected CSS. Click the button again to remove the CSS."
+ },
+ {
+ "javascript_apis": [
+ "extension.getURL",
+ "runtime.onMessage",
+ "tabs.executeScript",
+ "tabs.query",
+ "tabs.reload",
+ "tabs.sendMessage"
+ ],
+ "name": "beastify",
+ "description": "Adds a browser action icon to the toolbar. Click the button to choose a beast. The active tab's body content is then replaced with a picture of the chosen beast."
+ },
+ {
+ "javascript_apis": [
+ "bookmarks.create",
+ "bookmarks.onCreated",
+ "bookmarks.onRemoved",
+ "bookmarks.remove",
+ "bookmarks.search",
+ "browserAction.onClicked",
+ "browserAction.setIcon",
+ "tabs.onActivated",
+ "tabs.onUpdated",
+ "tabs.query",
+ "windows.onFocusChanged"
+ ],
+ "name": "bookmark-it",
+ "description": "Adds a bookmark button to the toolbar. Click the button to toggle a bookmark for the current page."
+ },
+ {
+ "javascript_apis": [],
+ "name": "borderify",
+ "description": "Adds a solid red border to all webpages matching mozilla.org."
+ },
+ {
+ "javascript_apis": [
+ "alarms.clearAll",
+ "alarms.create",
+ "alarms.onAlarm",
+ "pageAction.hide",
+ "pageAction.onClicked",
+ "pageAction.show",
+ "tabs.get",
+ "tabs.onActivated",
+ "tabs.onUpdated",
+ "tabs.query",
+ "tabs.update"
+ ],
+ "name": "chill-out",
+ "description": "Show a page action after a period of inactivity. Show cat gifs when the page action is clicked."
+ },
+ {
+ "javascript_apis": [
+ "commands.getAll",
+ "commands.onCommand"
+ ],
"name": "commands",
- "apis": [
- "getAll",
- "onCommand"
- ]
- }
- ]
- },
- {
- "name": "cookie-bg-picker",
- "description": "Allows the user to customize the background color and tiled pattern on sites the visit, and also saves their preferences via a cookie, reapplying them whenever they revisit a site they previously customized.",
- "url": "https://github.com/mdn/webextensions-examples/tree/master/cookie-bg-picker",
- "manifest_keys": [
- "permissions",
- "browser_action",
- "background",
- "web_accessible_resources"
- ],
- "javascript_modules": [
- {
- "name": "tabs",
- "apis": [
- "executeScript",
- "sendMessage",
- "query"
- ]
- },
- {
- "name": "cookies",
- "apis": [
- "Cookie",
- "get",
- "set",
- "remove",
- "onChanged"
- ]
- },
- {
- "name": "runtime",
- "apis": [
- "onMessage"
- ]
- }
- ]
- },
- {
- "name": "context-menu-demo",
- "description": "Demonstrates various features of the contextMenus API.",
- "url": "https://github.com/mdn/webextensions-examples/tree/master/context-menu-demo",
- "manifest_keys": [
- "permissions",
- "background"
- ],
- "javascript_modules": [
- {
- "name": "contextMenus",
- "apis": [
- "create",
- "onClicked",
- "update",
- "remove"
- ]
- },
- {
- "name": "i18n",
- "apis": [
- "getMessage"
- ]
- },
- {
- "name": "runtime",
- "apis": [
- "lastError"
- ]
- },
- {
- "name": "tabs",
- "apis": [
- "executeScript"
- ]
- }
- ]
- },
- {
- "name": "favourite-colour",
- "description": "An example options ui",
- "url": "https://github.com/mdn/webextensions-examples/tree/master/favourite-colour",
- "manifest_keys": [
- "permissions",
- "browser_action",
- "options_ui",
- "background",
- "storage"
- ],
- "javascript_modules": [
- {
- "name": "browserAction",
- "apis": [
- "onClicked"
- ]
- },
- {
- "name": "runtime",
- "apis": [
- "openOptionsPage"
- ]
- },
- {
- "name": "storage",
- "apis": [
- "StorageArea/get",
- "StorageArea/set"
- ]
- }
- ]
- },
- {
- "name": "history-deleter",
- "description": "History API demo: deletes history items for a given domain",
- "url": "https://github.com/mdn/webextensions-examples/tree/master/history-deleter",
- "manifest_keys": [
- "permissions",
- "page_action",
- "background"
- ],
- "javascript_modules": [
- {
- "name": "history",
- "apis": [
- "deleteUrl",
- "search"
- ]
- },
- {
- "name": "pageAction",
- "apis": [
- "show"
- ]
- },
- {
- "name": "tabs",
- "apis": [
- "onUpdated",
- "query"
- ]
- }
- ]
- },
- {
- "name": "inpage-toolbar-ui",
- "description": "Adds a browser action icon to the toolbar. Click the button to inject an in-page toolbar UI into the current webpage.",
- "url": "https://github.com/mdn/webextensions-examples/tree/master/inpage-toolbar-ui",
- "manifest_keys": [
- "permissions",
- "browser_action",
- "content_scripts",
- "web_accessible_resources",
- "background"
- ],
- "javascript_modules": [
- {
- "name": "runtime",
- "apis": [
- "getURL",
- "onConnect",
- "onMessage"
- ]
- },
- {
- "name": "browserAction",
- "apis": [
- "onClicked"
- ]
- },
- {
- "name": "tabs",
- "apis": [
- "sendMessage",
- "query"
- ]
- }
- ]
- },
- {
- "name": "latest-download",
- "description": "Shows the last downloaded item, and lets you open or delete it.",
- "url": "https://github.com/mdn/webextensions-examples/tree/master/latest-download",
- "manifest_keys": [
- "permissions",
- "browser_action"
- ],
- "javascript_modules": [
- {
- "name": "downloads",
- "apis": [
- "getFileIcon",
- "search",
- "open",
- "removeFile",
- "erase"
- ]
- },
- {
- "name": "runtime",
- "apis": [
- "lastError"
- ]
- }
- ]
- },
- {
- "name": "notify-link-clicks-i18n",
- "description": "Shows a notification when the user clicks on links.",
- "url": "https://github.com/mdn/webextensions-examples/tree/master/notify-link-clicks-i18n",
- "manifest_keys": [
- "permissions",
- "content_scripts",
- "default_locale",
- "background"
- ],
- "javascript_modules": [
- {
- "name": "i18n",
- "apis": [
- "getMessage"
- ]
- },
- {
- "name": "notifications",
- "apis": [
- "create"
- ]
- },
- {
- "name": "extension",
- "apis": [
- "getURL"
- ]
- },
- {
- "name": "runtime",
- "apis": [
- "onMessage",
- "sendMessage"
- ]
- }
- ]
- },
- {
- "name": "open-my-page-button",
- "description": "Adds browser action icon to toolbar to open packaged web page.",
- "url": "https://github.com/mdn/webextensions-examples/tree/master/open-my-page-button",
- "manifest_keys": [
- "browser_action",
- "background"
- ],
- "javascript_modules": [
- {
- "name": "browserAction",
- "apis": [
- "onClicked"
- ]
- },
- {
- "name": "tabs",
- "apis": [
- "create"
- ]
- },
- {
- "name": "extension",
- "apis": [
- "getURL"
- ]
- }
- ]
- },
- {
- "name": "page-to-extension-messaging",
- "description": "Visit https://mdn.github.io/webextensions-examples/content-script-page-script-messaging.html for the demo.",
- "url": "https://github.com/mdn/webextensions-examples/tree/master/page-to-extension-messaging",
- "manifest_keys": [
- "content_scripts"
- ],
- "javascript_modules": []
- },
- {
- "name": "quicknote",
- "description": "Allows the user to make quick notes by clicking a button and entering text into the resulting popup. The notes are saved in storage.",
- "url": "https://github.com/mdn/webextensions-examples/tree/master/quicknote",
- "manifest_keys": [
- "storage",
- "browser_action"
- ],
- "javascript_modules": [
- {
- "name": "storage",
- "apis": [
- "StorageArea/get",
- "StorageArea/set",
- "StorageArea/remove",
- "StorageArea/clear"
- ]
- }
- ]
- },
- {
- "name": "tabs-tabs-tabs",
- "description": "A list of methods you can perform on a tab.",
- "url": "https://github.com/mdn/webextensions-examples/tree/master/tabs-tabs-tabs",
- "manifest_keys": [
- "browser_action"
- ],
- "javascript_modules": [
- {
- "name": "tabs",
- "apis": [
- "query",
- "move",
- "duplicate",
- "reload",
- "remove"
- ]
- }
- ]
- },
- {
- "name": "user-agent-rewriter",
- "description": "Adds browser action icon to toolbar to choose user agent string from popup menu.",
- "url": "https://github.com/mdn/webextensions-examples/tree/master/user-agent-rewriter",
- "manifest_keys": [
- "browser_action",
- "background",
- "permissions"
- ],
- "javascript_modules": [
- {
- "name": "webRequest",
- "apis": [
- "onBeforeSendHeaders"
- ]
- },
- {
- "name": "extension",
- "apis": [
- "getBackgroundPage"
- ]
- }
- ]
- }
+ "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": [
+ "menus.create",
+ "menus.onClicked",
+ "tabs.executeScript"
+ ],
+ "name": "context-menu-copy-link-with-types",
+ "description": "Add a context menu option to links to copy the link to the clipboard, as plain text and as a link in rich HTML."
+ },
+ {
+ "javascript_apis": [
+ "menus.create",
+ "menus.onClicked",
+ "menus.remove",
+ "menus.update",
+ "i18n.getMessage",
+ "runtime.lastError",
+ "tabs.executeScript"
+ ],
+ "name": "menu-demo",
+ "description": "Demonstrates adding and manipulating menu items using the menus API."
+ },
+ {
+ "javascript_apis": [
+ "contextualIdentities.query",
+ "tabs.create",
+ "tabs.query",
+ "tabs.remove"
+ ],
+ "name": "contextual-identities",
+ "description": "List, create, and remove contextual identities."
+ },
+ {
+ "javascript_apis": [
+ "cookies.get",
+ "cookies.onChanged",
+ "cookies.remove",
+ "cookies.set",
+ "extension.getURL",
+ "runtime.onMessage",
+ "tabs.onActivated",
+ "tabs.onUpdated",
+ "tabs.query",
+ "tabs.sendMessage"
+ ],
+ "name": "cookie-bg-picker",
+ "description": "Allows the user to customize the background color and tiled pattern on sites the visit, and also saves their preferences via a cookie, reapplying them whenever they revisit a site they previously customized."
+ },
+ {
+ "javascript_apis": [
+ "alarms.create",
+ "alarms.onAlarm",
+ "theme.update"
+ ],
+ "name": "dynamic-theme",
+ "description": "Dynamic theme example"
+ },
+ {
+ "javascript_apis": [
+ "runtime.onMessage",
+ "runtime.sendMessage",
+ "storage.local"
+ ],
+ "name": "embedded-webextension-bootstrapped",
+ "description": "Demonstrates how to use an embedded WebExtension to port from a bootstrapped extension."
+ },
+ {
+ "javascript_apis": [
+ "notifications.create",
+ "runtime.connect",
+ "runtime.onConnect",
+ "runtime.onMessage",
+ "runtime.sendMessage",
+ "storage.local"
+ ],
+ "name": "embedded-webextension-sdk",
+ "description": "Demonstrates how to use an embedded WebExtension to port from an SDK-based add-on."
+ },
+ {
+ "javascript_apis": [],
+ "name": "emoji-substitution",
+ "description": "Replaces words with emojis."
+ },
+ {
+ "javascript_apis": [],
+ "name": "eslint-example",
+ "description": "Demonstrates how to configure an extension with eslint."
+ },
+ {
+ "javascript_apis": [
+ "browserAction.onClicked",
+ "runtime.openOptionsPage",
+ "storage.sync"
+ ],
+ "name": "favourite-colour",
+ "description": "An example options page, letting you store your favourite colour."
+ },
+ {
+ "javascript_apis": [
+ "omnibox.onInputChanged",
+ "omnibox.onInputEntered",
+ "omnibox.setDefaultSuggestion",
+ "tabs.create",
+ "tabs.update"
+ ],
+ "name": "firefox-code-search",
+ "description": "Demonstrates how to use the omnibox API."
+ },
+ {
+ "javascript_apis": [
+ "browserAction.onClicked",
+ "browsingData.remove",
+ "notifications.create",
+ "storage.local"
+ ],
+ "name": "forget-it",
+ "description": "Demonstrates how to use the browsingData API."
+ },
+ {
+ "javascript_apis": [
+ "browserAction.onClicked",
+ "identity.getRedirectURL",
+ "identity.launchWebAuthFlow",
+ "notifications.create"
+ ],
+ "name": "google-userinfo",
+ "description": "Demonstrates how to use the identity API."
+ },
+ {
+ "javascript_apis": [
+ "history.deleteUrl",
+ "history.search",
+ "pageAction.show",
+ "tabs.onUpdated",
+ "tabs.query"
+ ],
+ "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",
+ "downloads.getFileIcon",
+ "downloads.open",
+ "downloads.removeFile",
+ "downloads.search"
+ ],
+ "name": "latest-download",
+ "description": "Shows the last downloaded item, and lets you open or delete it."
+ },
+ {
+ "javascript_apis": [
+ "cookies.getAll",
+ "tabs.query"
+ ],
+ "name": "list-cookies",
+ "description": "This extensions list the cookies in the active tab."
+ },
+ {
+ "javascript_apis": [
+ "runtime.sendMessage"
+ ],
+ "name": "mocha-client-tests",
+ "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": [
+ "browserAction.onClicked",
+ "runtime.connectNative"
+ ],
+ "name": "native-messaging",
+ "description": "Example of native messaging, including a Python application and an extension which exchanges messages with it."
+ },
+ {
+ "javascript_apis": [
+ "storage.local",
+ "webNavigation.onCompleted"
+ ],
+ "name": "navigation-stats",
+ "description": "Demonstration of the webNavigation API, showing basic stats about which pages you've visited."
+ },
+ {
+ "javascript_apis": [
+ "extension.getURL",
+ "i18n.getMessage",
+ "notifications.create",
+ "runtime.onMessage",
+ "runtime.sendMessage"
+ ],
+ "name": "notify-link-clicks-i18n",
+ "description": "Shows a localized notification when the user clicks on links."
+ },
+ {
+ "javascript_apis": [
+ "browserAction.onClicked",
+ "tabs.create"
+ ],
+ "name": "open-my-page-button",
+ "description": "Adds a browser action icon to the toolbar. When the browser action is clicked, the add-on opens a page that was packaged with it."
+ },
+ {
+ "javascript_apis": [],
+ "name": "page-to-extension-messaging",
+ "description": "Demonstrates how a web page and a content script can exchange messages. Visit https://mdn.github.io/webextensions-examples/content-script-page-script-messaging.html for the demo."
+ },
+ {
+ "javascript_apis": [
+ "browserAction.onClicked",
+ "permissions.getAll",
+ "permissions.remove",
+ "permissions.request",
+ "runtime.getURL",
+ "tabs.create"
+ ],
+ "name": "permissions",
+ "description": "Demonstrates optional permissions using the permissions API."
+ },
+ {
+ "javascript_apis": [
+ "extension.getURL",
+ "proxy.onProxyError",
+ "proxy.register",
+ "runtime.onMessage",
+ "runtime.sendMessage",
+ "storage.local",
+ "storage.onChanged"
+ ],
+ "name": "proxy-blocker",
+ "description": "Uses the proxy API to block requests to specific hosts."
+ },
+ {
+ "javascript_apis": [
+ "storage.local"
+ ],
+ "name": "quicknote",
+ "description": "Allows the user to make quick notes by clicking a button and entering text into the resulting popup. The notes are saved in storage."
+ },
+ {
+ "javascript_apis": [],
+ "name": "react-es6-popup",
+ "description": "This is an example of creating a browser action popup UI in React and ES6 JavaScript."
+ },
+ {
+ "javascript_apis": [],
+ "name": "selection-to-clipboard",
+ "description": "Demonstrates how to write to the clipboard from a content script"
+ },
+ {
+ "javascript_apis": [
+ "storage.local",
+ "webRequest.onAuthRequired",
+ "webRequest.onCompleted",
+ "webRequest.onErrorOccurred"
+ ],
+ "name": "stored-credentials",
+ "description": "Performs basic authentication by supplying stored credentials."
+ },
+ {
+ "javascript_apis": [
+ "tabs.create",
+ "tabs.duplicate",
+ "tabs.getZoom",
+ "tabs.highlight",
+ "tabs.move",
+ "tabs.onMoved",
+ "tabs.onRemoved",
+ "tabs.query",
+ "tabs.reload",
+ "tabs.remove",
+ "tabs.setZoom"
+ ],
+ "name": "tabs-tabs-tabs",
+ "description": "Demonstrates tab manipulation: opening, closing, moving, zooming tabs."
+ },
+ {
+ "javascript_apis": [
+ "management.getAll",
+ "management.setEnabled"
+ ],
+ "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:
weta_fade: a basic theme employing a single image specified in headerURL:.
weta_fade_chrome: the weta_fade theme implemented with Chrome compatible manifest keys.
weta_tiled: a theme using a tiled image.
weta_mirror: a theme using multiple images and aligning those images in the header.
animated: use of an animated PNG.
"
+ },
+ {
+ "javascript_apis": [
+ "topSites.get"
+ ],
+ "name": "top-sites",
+ "description": "Demonstration of the topSites API."
+ },
+ {
+ "javascript_apis": [
+ "extension.getBackgroundPage",
+ "webRequest.onBeforeSendHeaders"
+ ],
+ "name": "user-agent-rewriter",
+ "description": "Demonstrates using the webRequest API to rewrite the User-Agent HTTP header."
+ },
+ {
+ "javascript_apis": [
+ "runtime.onMessage",
+ "runtime.sendMessage"
+ ],
+ "name": "webpack-modules",
+ "description": "Demonstrates how to use webpack to package npm modules in an extension."
+ },
+ {
+ "javascript_apis": [
+ "windows.create",
+ "windows.getAll",
+ "windows.getCurrent",
+ "windows.remove",
+ "windows.update"
+ ],
+ "name": "window-manipulator",
+ "description": "Demonstrates how to manipulate windows: opening, closing, resizing windows."
+ }
]
diff --git a/favourite-colour/background.js b/favourite-colour/background.js
index 86fa4ad..be08803 100644
--- a/favourite-colour/background.js
+++ b/favourite-colour/background.js
@@ -1,5 +1,5 @@
function handleClick() {
- chrome.runtime.openOptionsPage();
+ browser.runtime.openOptionsPage();
}
-chrome.browserAction.onClicked.addListener(handleClick);
+browser.browserAction.onClicked.addListener(handleClick);
diff --git a/favourite-colour/manifest.json b/favourite-colour/manifest.json
index 0c6c1d4..560c795 100644
--- a/favourite-colour/manifest.json
+++ b/favourite-colour/manifest.json
@@ -1,10 +1,4 @@
{
- "applications": {
- "gecko": {
- "id": "some-options@mozilla.org",
- "strict_min_version": "48.0a1"
- }
- },
"background": {
"scripts": ["background.js"]
},
@@ -16,8 +10,14 @@
"manifest_version": 2,
"name": "Favourite colour",
"options_ui": {
- "page": "options.html"
+ "page": "options.html",
+ "browser_style": true
},
"permissions": ["storage"],
- "version": "1.0"
+ "version": "1.1",
+ "applications": {
+ "gecko": {
+ "id": "favourite-colour-examples@mozilla.org"
+ }
+ }
}
diff --git a/favourite-colour/options.html b/favourite-colour/options.html
index ed9a7bf..77c925e 100644
--- a/favourite-colour/options.html
+++ b/favourite-colour/options.html
@@ -7,12 +7,9 @@
diff --git a/favourite-colour/options.js b/favourite-colour/options.js
index 5693865..5c23b75 100644
--- a/favourite-colour/options.js
+++ b/favourite-colour/options.js
@@ -1,11 +1,13 @@
function saveOptions(e) {
- chrome.storage.local.set({
+ browser.storage.sync.set({
colour: document.querySelector("#colour").value
});
+ e.preventDefault();
}
function restoreOptions() {
- chrome.storage.local.get('colour', (res) => {
+ var gettingItem = browser.storage.sync.get('colour');
+ gettingItem.then((res) => {
document.querySelector("#colour").value = res.colour || 'Firefox red';
});
}
diff --git a/firefox-code-search/README.md b/firefox-code-search/README.md
new file mode 100644
index 0000000..19e9681
--- /dev/null
+++ b/firefox-code-search/README.md
@@ -0,0 +1,13 @@
+# firefox-code-search
+
+## What it does
+
+This extension allows you to search the Firefox codebase using the awesome bar.
+
+To test it out, type 'cs' into the awesome bar followed by a search string (e.g. `cs hello world`). The results will be shown as suggestions, and clicking on a suggestion will navigate to the file where the result was found.
+
+To search a specific file, use the "path:" prefix (e.g. `cs hello path:omnibox.js`)
+
+## What it shows
+
+How to use the omnibox API to add custom suggestions to the awesome bar.
\ No newline at end of file
diff --git a/firefox-code-search/background.js b/firefox-code-search/background.js
new file mode 100644
index 0000000..a467a47
--- /dev/null
+++ b/firefox-code-search/background.js
@@ -0,0 +1,101 @@
+const BASE_URL = "https://searchfox.org/mozilla-central";
+const SEARCH_URL = `${BASE_URL}/search`;
+const SOURCE_URL = `${BASE_URL}/source`;
+
+// Provide help text to the user.
+browser.omnibox.setDefaultSuggestion({
+ description: `Search the firefox codebase
+ (e.g. "hello world" | "path:omnibox.js onInputChanged")`
+});
+
+// Update the suggestions whenever the input is changed.
+browser.omnibox.onInputChanged.addListener((text, addSuggestions) => {
+ let headers = new Headers({"Accept": "application/json"});
+ let init = {method: 'GET', headers};
+ let url = buildSearchURL(text);
+ let request = new Request(url, init);
+
+ fetch(request)
+ .then(createSuggestionsFromResponse)
+ .then(addSuggestions);
+});
+
+// Open the page based on how the user clicks on a suggestion.
+browser.omnibox.onInputEntered.addListener((text, disposition) => {
+ let url = text;
+ if (!text.startsWith(SOURCE_URL)) {
+ // Update the url if the user clicks on the default suggestion.
+ url = `${SEARCH_URL}?q=${text}`;
+ }
+ switch (disposition) {
+ case "currentTab":
+ browser.tabs.update({url});
+ break;
+ case "newForegroundTab":
+ browser.tabs.create({url});
+ break;
+ case "newBackgroundTab":
+ browser.tabs.create({url, active: false});
+ break;
+ }
+});
+
+function buildSearchURL(text) {
+ let path = '';
+ let queryParts = [];
+ let query = '';
+ let parts = text.split(' ');
+
+ parts.forEach(part => {
+ if (part.startsWith("path:")) {
+ path = part.slice(5);
+ } else {
+ queryParts.push(part);
+ }
+ });
+
+ query = queryParts.join(' ');
+ return `${SEARCH_URL}?q=${query}&path=${path}`;
+}
+
+function createSuggestionsFromResponse(response) {
+ return new Promise(resolve => {
+ let suggestions = [];
+ let suggestionsOnEmptyResults = [{
+ content: SOURCE_URL,
+ description: "no results found"
+ }];
+ response.json().then(json => {
+ if (!json.normal) {
+ return resolve(suggestionsOnEmptyResults);
+ }
+
+ let occurrences = json.normal["Textual Occurrences"];
+ let files = json.normal["Files"];
+
+ if (!occurrences && !files) {
+ return resolve(suggestionsOnEmptyResults);
+ }
+
+ if (occurrences) {
+ occurrences.forEach(({path, lines}) => {
+ suggestions.push({
+ content: `${SOURCE_URL}/${path}#${lines[0].lno}`,
+ description: lines[0].line,
+ });
+ });
+ return resolve(suggestions);
+ }
+
+ // There won't be any textual occurrences if the "path:" prefix is used.
+ files.forEach(({path}) => {
+ suggestions.push({
+ content: `${SOURCE_URL}/${path}`,
+ description: path,
+ });
+ });
+ return resolve(suggestions);
+ });
+ });
+}
+
diff --git a/firefox-code-search/manifest.json b/firefox-code-search/manifest.json
new file mode 100644
index 0000000..38c6bad
--- /dev/null
+++ b/firefox-code-search/manifest.json
@@ -0,0 +1,18 @@
+{
+ "name": "Firefox Code Search",
+ "description" : "To use, type 'cs' plus a search term into the url bar.",
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "52.0a1"
+ }
+ },
+ "background": {
+ "scripts": ["background.js"]
+ },
+ "omnibox": { "keyword" : "cs" },
+ "manifest_version": 2,
+ "permissions": [
+ "https://searchfox.org/"
+ ]
+}
diff --git a/forget-it/README.md b/forget-it/README.md
new file mode 100644
index 0000000..9fd03d0
--- /dev/null
+++ b/forget-it/README.md
@@ -0,0 +1,5 @@
+# Forget it!
+
+This add-on adds a button to the browser's toolbar. When the user clicks the button, the add-on clears some browsing data using the browsingData API. The details of what exactly it clears are determined by the add-on's settings, which can be accessed and modified in its options page.
+
+The example shows how to use the browsingData API, and how to use the options page to handle settings.
diff --git a/forget-it/background.js b/forget-it/background.js
new file mode 100644
index 0000000..9390e11
--- /dev/null
+++ b/forget-it/background.js
@@ -0,0 +1,89 @@
+/*
+Default settings. If there is nothing in storage, use these values.
+*/
+var defaultSettings = {
+ since: "hour",
+ dataTypes: ["history", "downloads"]
+};
+
+/*
+Generic error logger.
+*/
+function onError(e) {
+ console.error(e);
+}
+
+/*
+On startup, check whether we have stored settings.
+If we don't, then store the default settings.
+*/
+function checkStoredSettings(storedSettings) {
+ if (!storedSettings.since || !storedSettings.dataTypes) {
+ browser.storage.local.set(defaultSettings);
+ }
+}
+
+const gettingStoredSettings = browser.storage.local.get();
+gettingStoredSettings.then(checkStoredSettings, onError);
+
+/*
+Forget browsing data, according to the settings passed in as storedSettings
+or, if this is empty, according to the default settings.
+*/
+function forget(storedSettings) {
+
+ /*
+ Convert from a string to a time.
+ The string is one of: "hour", "day", "week", "forever".
+ The time is given in milliseconds since the epoch.
+ */
+ function getSince(selectedSince) {
+ if (selectedSince === "forever") {
+ return 0;
+ }
+
+ const times = {
+ hour: () => { return 1000 * 60 * 60 },
+ day: () => { return 1000 * 60 * 60 * 24 },
+ week: () => { return 1000 * 60 * 60 * 24 * 7}
+ }
+
+ const sinceMilliseconds = times[selectedSince].call();
+ return Date.now() - sinceMilliseconds;
+ }
+
+ /*
+ Convert from an array of strings, representing data types,
+ to an object suitable for passing into browsingData.remove().
+ */
+ function getTypes(selectedTypes) {
+ let dataTypes = {};
+ for (let item of selectedTypes) {
+ dataTypes[item] = true;
+ }
+ return dataTypes;
+ }
+
+ const since = getSince(storedSettings.since);
+ const dataTypes = getTypes(storedSettings.dataTypes);
+
+ function notify() {
+ let dataTypesString = Object.keys(dataTypes).join(", ");
+ let sinceString = new Date(since).toLocaleString();
+ browser.notifications.create({
+ "type": "basic",
+ "title": "Removed browsing data",
+ "message": `Removed ${dataTypesString}\nsince ${sinceString}`
+ });
+ }
+
+ browser.browsingData.remove({since}, dataTypes).then(notify);
+}
+
+/*
+On click, fetch stored settings and forget browsing data.
+*/
+browser.browserAction.onClicked.addListener(() => {
+ const gettingStoredSettings = browser.storage.local.get();
+ gettingStoredSettings.then(forget, onError);
+});
diff --git a/forget-it/icons/LICENSE b/forget-it/icons/LICENSE
new file mode 100644
index 0000000..a37916c
--- /dev/null
+++ b/forget-it/icons/LICENSE
@@ -0,0 +1 @@
+The trash.svg” icon is taken from the miu iconset and is used under the terms of its license: https://www.iconfinder.com/iconsets/miu.
diff --git a/forget-it/icons/trash.svg b/forget-it/icons/trash.svg
new file mode 100644
index 0000000..072d731
--- /dev/null
+++ b/forget-it/icons/trash.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/forget-it/manifest.json b/forget-it/manifest.json
new file mode 100644
index 0000000..f540a8d
--- /dev/null
+++ b/forget-it/manifest.json
@@ -0,0 +1,30 @@
+{
+
+ "description": "Forget it!",
+ "manifest_version": 2,
+ "name": "forget-it",
+ "version": "2.0",
+ "homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/forget-it",
+ "icons": {
+ "48": "icons/trash.svg"
+ },
+
+ "background": {
+ "scripts": ["background.js"]
+ },
+
+ "browser_action": {
+ "default_icon": "icons/trash.svg",
+ "default_title": "Forget it!"
+ },
+
+ "options_ui": {
+ "page": "options/options.html"
+ },
+
+ "permissions": [
+ "browsingData",
+ "notifications",
+ "storage"
+ ]
+}
diff --git a/forget-it/options/options.css b/forget-it/options/options.css
new file mode 100644
index 0000000..d224a51
--- /dev/null
+++ b/forget-it/options/options.css
@@ -0,0 +1,36 @@
+
+body {
+ width: 25em;
+ font-family: "Open Sans Light", sans-serif;
+ font-size: 0.9em;
+ font-weight: 300;
+}
+
+section.clear-options {
+ padding: 0.5em 0;
+ margin: 1em 0;
+}
+
+#clear-button {
+ margin: 0 1.3em 1em 0;
+}
+
+section.clear-options input,
+section.clear-options>select,
+#clear-button {
+ float: right;
+}
+
+label {
+ display: block;
+ padding: 0.2em 0;
+}
+
+label:hover {
+ background-color: #EAEFF2;
+}
+
+.title {
+ font-size: 1.2em;
+ margin-bottom: 0.5em;
+}
diff --git a/forget-it/options/options.html b/forget-it/options/options.html
new file mode 100644
index 0000000..aea0636
--- /dev/null
+++ b/forget-it/options/options.html
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+ Time range to forget:
+
+
+
+
+
Type of data to forget:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/forget-it/options/options.js b/forget-it/options/options.js
new file mode 100644
index 0000000..aad693e
--- /dev/null
+++ b/forget-it/options/options.js
@@ -0,0 +1,62 @@
+/*
+Store the currently selected settings using browser.storage.local.
+*/
+function storeSettings() {
+
+ function getSince() {
+ const since = document.querySelector("#since");
+ return since.value;
+ }
+
+ function getTypes() {
+ let dataTypes = [];
+ const checkboxes = document.querySelectorAll(".data-types [type=checkbox]");
+ for (let item of checkboxes) {
+ if (item.checked) {
+ dataTypes.push(item.getAttribute("data-type"));
+ }
+ }
+ return dataTypes;
+ }
+
+ const since = getSince();
+ const dataTypes = getTypes();
+ browser.storage.local.set({
+ since,
+ dataTypes
+ });
+}
+
+/*
+Update the options UI with the settings values retrieved from storage,
+or the default settings if the stored settings are empty.
+*/
+function updateUI(restoredSettings) {
+ const selectList = document.querySelector("#since");
+ selectList.value = restoredSettings.since;
+
+ const checkboxes = document.querySelectorAll(".data-types [type=checkbox]");
+ for (let item of checkboxes) {
+ if (restoredSettings.dataTypes.indexOf(item.getAttribute("data-type")) != -1) {
+ item.checked = true;
+ } else {
+ item.checked = false;
+ }
+ }
+}
+
+function onError(e) {
+ console.error(e);
+}
+
+/*
+On opening the options page, fetch stored settings and update the UI with them.
+*/
+const gettingStoredSettings = browser.storage.local.get();
+gettingStoredSettings.then(updateUI, onError);
+
+/*
+On clicking the save button, save the currently selected settings.
+*/
+const saveButton = document.querySelector("#save-button");
+saveButton.addEventListener("click", storeSettings);
diff --git a/google-userinfo/README.md b/google-userinfo/README.md
new file mode 100644
index 0000000..ff41025
--- /dev/null
+++ b/google-userinfo/README.md
@@ -0,0 +1,33 @@
+# google-userinfo
+
+This add-on fetches the user's info from their Google account and displays their name in a notification.
+
+In detail, it adds a browser action. When the user clicks the browser action, the add-on:
+
+* uses `identity.launchWebAuthFlow()` to get an access token from Google. This asks the user to sign into Google, if they are not already signed in (authentication), then asks the user if they grant the WebExtension permission to get their user info, if the user has not already granted this permission (authorization).
+
+* validates the access token
+
+* passes the access token into a Google API that returns the user's info
+
+* displays a notification containing the user's name.
+
+This is following essentially the process documented here: https://developers.google.com/identity/protocols/OAuth2UserAgent.
+
+## Setup ##
+
+There's some basic setup you must do before you can use this example.
+
+* **getting the redirect URL**: this represents the end point of the flow, where the access token is delivered to the WebExtension. The redirect URL is derived from the WebExtension's ID. To get the redirect URL for this example, install it, visit about:addons, and open its "Preferences" page. It will look something like "https://dc6ae45f54e3d55036b819b93a1876228e5f5f7b.extensions.allizom.org/".
+
+* **registering your add-on with Google as an OAuth2 client**.
+ * Visit https://console.developers.google.com/apis/credentials
+ * Click "Create credentials", and select "OAuth client ID"
+ * Select "Web application", and give it a name. The name is shown to the user to help them understand whether to authorize the add-on.
+ * Paste the redirect URL into the " Authorized redirect URIs" box.
+ * Click "Create"
+ * You'll see a popup containing a Client ID and a secret. Copy the client ID (you can ignore the secret).
+ * Paste this value into authorize.js in place of YOUR-CLIENT-ID.
+ * Reload the add-on.
+
+Note that because you have to edit authorize.js, we can't provide a prebuilt, presigned version of this add-on in the "builds" directory of this repo, as we can for other examples. So to run this example in Firefox you'll need to use the ["Load Temporary Add-on"](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Temporary_Installation_in_Firefox) feature, or use the [web-ext](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Getting_started_with_web-ext) tool.
diff --git a/google-userinfo/background/authorize.js b/google-userinfo/background/authorize.js
new file mode 100644
index 0000000..3b349c4
--- /dev/null
+++ b/google-userinfo/background/authorize.js
@@ -0,0 +1,76 @@
+/* exported getAccessToken */
+
+const REDIRECT_URL = browser.identity.getRedirectURL();
+const CLIENT_ID = "YOUR-CLIENT-ID";
+const SCOPES = ["openid", "email", "profile"];
+const AUTH_URL =
+`https://accounts.google.com/o/oauth2/auth
+?client_id=${CLIENT_ID}
+&response_type=token
+&redirect_uri=${encodeURIComponent(REDIRECT_URL)}
+&scope=${encodeURIComponent(SCOPES.join(' '))}`;
+const VALIDATION_BASE_URL="https://www.googleapis.com/oauth2/v3/tokeninfo";
+
+function extractAccessToken(redirectUri) {
+ let m = redirectUri.match(/[#?](.*)/);
+ if (!m || m.length < 1)
+ return null;
+ let params = new URLSearchParams(m[1].split("#")[0]);
+ return params.get("access_token");
+}
+
+/**
+Validate the token contained in redirectURL.
+This follows essentially the process here:
+https://developers.google.com/identity/protocols/OAuth2UserAgent#tokeninfo-validation
+- make a GET request to the validation URL, including the access token
+- if the response is 200, and contains an "aud" property, and that property
+matches the clientID, then the response is valid
+- otherwise it is not valid
+
+Note that the Google page talks about an "audience" property, but in fact
+it seems to be "aud".
+*/
+function validate(redirectURL) {
+ const accessToken = extractAccessToken(redirectURL);
+ if (!accessToken) {
+ throw "Authorization failure";
+ }
+ const validationURL = `${VALIDATION_BASE_URL}?access_token=${accessToken}`;
+ const validationRequest = new Request(validationURL, {
+ method: "GET"
+ });
+
+ function checkResponse(response) {
+ return new Promise((resolve, reject) => {
+ if (response.status != 200) {
+ reject("Token validation error");
+ }
+ response.json().then((json) => {
+ if (json.aud && (json.aud === CLIENT_ID)) {
+ resolve(accessToken);
+ } else {
+ reject("Token validation error");
+ }
+ });
+ });
+ }
+
+ return fetch(validationRequest).then(checkResponse);
+}
+
+/**
+Authenticate and authorize using browser.identity.launchWebAuthFlow().
+If successful, this resolves with a redirectURL string that contains
+an access token.
+*/
+function authorize() {
+ return browser.identity.launchWebAuthFlow({
+ interactive: true,
+ url: AUTH_URL
+ });
+}
+
+function getAccessToken() {
+ return authorize().then(validate);
+}
diff --git a/google-userinfo/background/main.js b/google-userinfo/background/main.js
new file mode 100644
index 0000000..9ddf1fb
--- /dev/null
+++ b/google-userinfo/background/main.js
@@ -0,0 +1,25 @@
+/*global getAccessToken*/
+
+function notifyUser(user) {
+ browser.notifications.create({
+ "type": "basic",
+ "title": "Google info",
+ "message": `Hi ${user.name}`
+ });}
+
+function logError(error) {
+ console.error(`Error: ${error}`);
+}
+
+/**
+When the button's clicked:
+- get an access token using the identity API
+- use it to get the user's info
+- show a notification containing some of it
+*/
+browser.browserAction.onClicked.addListener(() => {
+ getAccessToken()
+ .then(getUserInfo)
+ .then(notifyUser)
+ .catch(logError);
+});
diff --git a/google-userinfo/background/userinfo.js b/google-userinfo/background/userinfo.js
new file mode 100644
index 0000000..5f7ac1d
--- /dev/null
+++ b/google-userinfo/background/userinfo.js
@@ -0,0 +1,25 @@
+/**
+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();
+ requestHeaders.append('Authorization', 'Bearer ' + accessToken);
+ const driveRequest = new Request(requestURL, {
+ method: "GET",
+ headers: requestHeaders
+ });
+
+ return fetch(driveRequest).then((response) => {
+ if (response.status === 200) {
+ return response.json();
+ } else {
+ throw response.status;
+ }
+ });
+
+}
diff --git a/google-userinfo/icons/LICENSE b/google-userinfo/icons/LICENSE
new file mode 100644
index 0000000..18fae49
--- /dev/null
+++ b/google-userinfo/icons/LICENSE
@@ -0,0 +1 @@
+The "person-32.png" "person-48.png" icons are taken from the Ionicons iconset (http://ionicons.com/), and are used here under the MIT license: http://opensource.org/licenses/MIT.
diff --git a/google-userinfo/icons/person-32.png b/google-userinfo/icons/person-32.png
new file mode 100644
index 0000000..38a16bd
Binary files /dev/null and b/google-userinfo/icons/person-32.png differ
diff --git a/google-userinfo/icons/person-48.png b/google-userinfo/icons/person-48.png
new file mode 100644
index 0000000..0cb787b
Binary files /dev/null and b/google-userinfo/icons/person-48.png differ
diff --git a/google-userinfo/manifest.json b/google-userinfo/manifest.json
new file mode 100644
index 0000000..e582748
--- /dev/null
+++ b/google-userinfo/manifest.json
@@ -0,0 +1,41 @@
+{
+
+ "name": "Google User Info",
+ "version": "1",
+ "manifest_version": 2,
+ "applications": {
+ "gecko": {
+ "id": "google-user-info@mozilla.org",
+ "strict_min_version": "53a1"
+ }
+ },
+
+ "icons": {
+ "48": "icons/person-48.png"
+ },
+
+ "browser_action": {
+ "browser_style": true,
+ "default_icon": "icons/person-32.png"
+ },
+
+ "permissions": [
+ "identity",
+ "notifications",
+ "*://www.googleapis.com/*",
+ "*://accounts.google.com/*"
+ ],
+
+ "background": {
+ "scripts": [
+ "background/authorize.js",
+ "background/userinfo.js",
+ "background/main.js"
+ ]
+ },
+
+ "options_ui": {
+ "page": "options/options.html"
+ }
+
+}
diff --git a/google-userinfo/options/options.html b/google-userinfo/options/options.html
new file mode 100644
index 0000000..9920792
--- /dev/null
+++ b/google-userinfo/options/options.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/history-deleter/history.js b/history-deleter/history.js
index 6e5c31e..798f8db 100644
--- a/history-deleter/history.js
+++ b/history-deleter/history.js
@@ -2,55 +2,71 @@
function get_hostname(url) {
var a = document.createElement('a');
a.href = url;
+ set_domain(a.hostname);
return a.hostname;
}
+function set_domain(domain) {
+ const spans = document.getElementsByClassName('domain');
+ [].slice.call(spans).forEach((span) => {
+ span.textContent = domain;
+ });
+}
+
function no_history(hostname) {
- document.getElementById('history').innerHTML = `No history for ${hostname}.`;
+ var history_text = document.getElementById('history');
+ while(history_text.firstChild)
+ history_text.removeChild(history_text.firstChild);
+ history_text.textContent = `No history for ${hostname}.`;
+}
+
+function getActiveTab() {
+ return browser.tabs.query({active: true, currentWindow: true});
}
// When the page is loaded find the current tab and then use that to query
// the history.
-chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
+getActiveTab().then((tabs) => {
var list = document.getElementById('history');
var hostname = get_hostname(tabs[0].url);
- chrome.history.search(
- // Search for all history entries for the current windows domain.
- {text: hostname, maxResults: 5},
- function(results) {
- // What to show if there are no results.
- if (results.length < 1) {
- no_history(hostname);
- } else {
- // Because this could be a lot of entries, lets limit it to 5.
- for (var k in results) {
- var history = results[k];
- var li = document.createElement('p');
- var url = document.createTextNode(history.url);
- li.appendChild(url);
- list.appendChild(li);
- }
+ // Search for all history entries for the current windows domain.
+ // Because this could be a lot of entries, lets limit it to 5.
+ var searchingHistory = browser.history.search({text: hostname, maxResults: 5});
+ searchingHistory.then((results) => {
+ // What to show if there are no results.
+ if (results.length < 1) {
+ no_history(hostname);
+ } else {
+ for (var k in results) {
+ var history = results[k];
+ var li = document.createElement('p');
+ var a = document.createElement('a');
+ var url = document.createTextNode(history.url);
+ a.href = history.url;
+ a.target = '_blank';
+ a.appendChild(url);
+ li.appendChild(a);
+ list.appendChild(li);
}
}
- );
+ });
});
function clearAll(e) {
- chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
+ getActiveTab().then((tabs) => {
var hostname = get_hostname(tabs[0].url);
if (!hostname) {
// Don't try and delete history when there's no hostname.
return;
}
- chrome.history.search(
- {text: hostname},
- // Search will return us a list of histories for this domain.
- // Loop through them and delete them one by one.
- function(results) {
- for (k = 0; k < results.length; k++) {
- chrome.history.deleteUrl({url: results[k].url});
+ // Search will return us a list of histories for this domain.
+ // Loop through them and delete them one by one.
+ var searchingHistory = browser.history.search({text: hostname})
+ searchingHistory.then((results) => {
+ for (let k of results) {
+ browser.history.deleteUrl({url: results[k].url});
}
// Clear out the UI.
no_history(hostname);
diff --git a/history-deleter/manifest.json b/history-deleter/manifest.json
index 11bb77b..de0d8b9 100644
--- a/history-deleter/manifest.json
+++ b/history-deleter/manifest.json
@@ -1,10 +1,4 @@
{
- "applications": {
- "gecko": {
- "id": "history-deleter@mozilla.com",
- "strict_min_version": "49.0a2"
- }
- },
"background": {
"scripts": ["background.js"]
},
diff --git a/http-response/README.md b/http-response/README.md
new file mode 100755
index 0000000..40c2338
--- /dev/null
+++ b/http-response/README.md
@@ -0,0 +1,11 @@
+# HTTP Response parser
+
+## What it does
+
+Listens to HTTP Responses from example.com and changes the body of the response as it comes through. So that the word "Example" on https://example.com becomes "WebExtension Example".
+
+## What it shows
+
+How to use the response parser on bytes.
+
+Icon is from: https://www.iconfinder.com/icons/763339/draw_edit_editor_pen_pencil_tool_write_icon#size=128
diff --git a/http-response/background.js b/http-response/background.js
new file mode 100755
index 0000000..b7a7880
--- /dev/null
+++ b/http-response/background.js
@@ -0,0 +1,22 @@
+function listener(details) {
+ let filter = browser.webRequest.filterResponseData(details.requestId);
+ let decoder = new TextDecoder("utf-8");
+ let encoder = new TextEncoder();
+
+ filter.ondata = event => {
+ let str = decoder.decode(event.data, {stream: true});
+ // Just change any instance of Example in the HTTP response
+ // to WebExtension Example.
+ str = str.replace(/Example/g, 'WebExtension Example');
+ filter.write(encoder.encode(str));
+ filter.disconnect();
+ }
+
+ return {};
+}
+
+browser.webRequest.onBeforeRequest.addListener(
+ listener,
+ {urls: ["https://example.com/*"], types: ["main_frame"]},
+ ["blocking"]
+);
diff --git a/http-response/manifest.json b/http-response/manifest.json
new file mode 100755
index 0000000..e2a13f4
--- /dev/null
+++ b/http-response/manifest.json
@@ -0,0 +1,26 @@
+{
+
+ "description": "Altering HTTP responses",
+ "manifest_version": 2,
+ "name": "http-response-filter",
+ "version": "1.0",
+ "homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/http-response",
+ "icons": {
+ "48": "pen.svg"
+ },
+
+ "permissions": [
+ "webRequest", "webRequestBlocking", "https://example.com/*"
+ ],
+
+ "background": {
+ "scripts": ["background.js"]
+ },
+
+ "applications": {
+ "gecko": {
+ "strict_min_version": "57.0a1"
+ }
+ }
+
+}
diff --git a/http-response/pen.svg b/http-response/pen.svg
new file mode 100755
index 0000000..65a3f42
--- /dev/null
+++ b/http-response/pen.svg
@@ -0,0 +1 @@
+
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/inpage-toolbar-ui/README.md b/inpage-toolbar-ui/README.md
deleted file mode 100644
index 29d5b80..0000000
--- a/inpage-toolbar-ui/README.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# inpage-toolbar-ui
-
-## What it does ##
-
-The extension includes:
-
-* a browser action which enables/disables the in-page toolbar
-* a content script which creates/removes the in-page toolbar iframe
-* the toolbar ui resources, packaged as web accessible resources
-
-When the user clicks the browser action button, a toolbar is shown/hidden
-in the current web page.
-
-The toolbar UI is packaged in the add-on resources, exposed to the current
-web page as a web accessible resource and injected into the page by the
-content script by creating and injecting into the page an iframe which
-points to the toolbar UI page.
-
-## What it shows ##
-
-How to expose an in-page toolbar UI by creating an iframe:
-
-* use web accessible resources to enable web pages to load packaged content
-* use a content script to create and inject in a web page an iframe which points to the
- packaged content
-* use the same API enabled in content scripts (but from the add-on iframe)
- to exchange messages directly with the add-on background page
diff --git a/inpage-toolbar-ui/background.js b/inpage-toolbar-ui/background.js
deleted file mode 100644
index 49aab61..0000000
--- a/inpage-toolbar-ui/background.js
+++ /dev/null
@@ -1,17 +0,0 @@
-// Send a message to the current tab's content script.
-function toggleToolbar() {
- chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) {
- chrome.tabs.sendMessage(tabs[0].id, "toggle-in-page-toolbar");
- });
-}
-
-// Handle the browser action button.
-chrome.browserAction.onClicked.addListener(toggleToolbar);
-
-// Handle connections received from the add-on toolbar ui iframes.
-chrome.runtime.onConnect.addListener(function (port) {
- if (port.sender.url == chrome.runtime.getURL("toolbar/ui.html")) {
- // Handle port messages received from the connected toolbar ui frames.
- port.onMessage.addListener(toggleToolbar);
- }
-});
diff --git a/inpage-toolbar-ui/contentscript.js b/inpage-toolbar-ui/contentscript.js
deleted file mode 100644
index e99e84e..0000000
--- a/inpage-toolbar-ui/contentscript.js
+++ /dev/null
@@ -1,36 +0,0 @@
-var toolbarUI;
-
-// Create the toolbar ui iframe and inject it in the current page
-function initToolbar() {
- var iframe = document.createElement("iframe");
- iframe.setAttribute("src", chrome.runtime.getURL("toolbar/ui.html"));
- iframe.setAttribute("style", "position: fixed; top: 0; left: 0; z-index: 10000; width: 100%; height: 36px;");
- document.body.appendChild(iframe);
-
- return toolbarUI = {
- iframe: iframe, visible: true
- };
-}
-
-function toggleToolbar(toolbarUI) {
- if (toolbarUI.visible) {
- toolbarUI.visible = false;
- toolbarUI.iframe.style["display"] = "none";
- } else {
- toolbarUI.visible = true;
- toolbarUI.iframe.style["display"] = "block";
- }
-}
-
-// Handle messages from the add-on background page (only in top level iframes)
-if (window.parent == window) {
- chrome.runtime.onMessage.addListener(function(msg) {
- if (msg == "toggle-in-page-toolbar") {
- if (toolbarUI) {
- toggleToolbar(toolbarUI);
- } else {
- toolbarUI = initToolbar();
- }
- }
- });
-}
diff --git a/inpage-toolbar-ui/icons/32.png b/inpage-toolbar-ui/icons/32.png
deleted file mode 100644
index 20345a7..0000000
Binary files a/inpage-toolbar-ui/icons/32.png and /dev/null differ
diff --git a/inpage-toolbar-ui/icons/48.png b/inpage-toolbar-ui/icons/48.png
deleted file mode 100644
index e9896d2..0000000
Binary files a/inpage-toolbar-ui/icons/48.png and /dev/null differ
diff --git a/inpage-toolbar-ui/manifest.json b/inpage-toolbar-ui/manifest.json
deleted file mode 100644
index c0fe3f7..0000000
--- a/inpage-toolbar-ui/manifest.json
+++ /dev/null
@@ -1,40 +0,0 @@
-{
- "description": "Adds a browser action icon to the toolbar. Click the button to inject an in-page toolbar UI into the current webpage. See https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Examples#inpage-toolbar-ui",
- "manifest_version": 2,
- "name": "In Page Toolbar UI",
- "version": "1.0",
- "homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/inpage-toolbar-ui",
- "icons": {
- "48": "icons/48.png"
- },
-
- "permissions": [],
-
- "background": {
- "scripts": ["background.js"]
- },
-
- "browser_action": {
- "default_icon": "icons/32.png",
- "default_title": "In Page Toolbar"
- },
-
- "content_scripts": [
- {
- "js": ["contentscript.js"],
- "run_at": "document_idle",
- "matches": [""]
- }
- ],
-
- "web_accessible_resources": [
- "toolbar/ui.html"
- ],
-
- "applications": {
- "gecko": {
- "id": "inpage-toolbar-ui@mozilla.org",
- "strict_min_version": "46.0"
- }
- }
-}
diff --git a/inpage-toolbar-ui/toolbar/ui.css b/inpage-toolbar-ui/toolbar/ui.css
deleted file mode 100644
index 9a81ae4..0000000
--- a/inpage-toolbar-ui/toolbar/ui.css
+++ /dev/null
@@ -1,62 +0,0 @@
-body {
- overflow: hidden;
- color: black;
- background: rgba(255,255,255,0.9);
-}
-
-#rainbow {
- background: linear-gradient(0deg, rgba(217,26,18,0.70) 15%, rgba(225,51,0,0.70) 15%, rgba(255, 127, 20, 0.70) 16%, rgba(242, 171, 3, 0.70) 32%, rgba(235, 192, 0, 0.70) 32%, rgba(250, 222, 0, 0.70) 33%, rgba(239, 255, 3, 0.70) 48%, rgba(86, 252, 2, 0.70) 49%, rgba(82, 255, 1, 0.70) 66%, rgba(74, 222, 126, 0.70) 67%, rgba(59, 170, 242, 0.70) 67%, rgba(59, 170, 242, 0.70) 84%, rgba(115, 55, 247, 0.70) 84%, rgba(107, 64, 242, 0.70) 100%);
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 6px;
-}
-
-#title {
- font-weight: bold;
- font-size: 1em;
-}
-
-.whimsy {
- width: 36px;
- height: 36px;
-}
-
-/* Toggle Button. */
-
-#toggle {
- top: 8px;
- right: 4px;
- position: absolute;
- margin: 0 1em;
- text-decoration: underline;
- border-radius: 1em;
- background: transparent linear-gradient(0deg, rgb(255, 162, 0), rgba(189, 122, 6, 0.66));
-}
-
-/* Annoying animation. */
-
-@keyframes wobbling {
- 50% {
- transform: translateY(-13px);
- }
-
- 100% {
- transform: translateY(0px);
- }
-}
-
-.wobbling {
- display: inline-block;
- vertical-align: middle;
- transform: translateZ(0);
- box-shadow: 0 0 1px rgba(0, 0, 0, 0);
- backface-visibility: hidden;
-
- animation-name: wobbling;
- animation-duration: 1.25s;
- animation-timing-function: linear;
- animation-iteration-count: infinite;
- animation-fill-mode: both;
-}
diff --git a/inpage-toolbar-ui/toolbar/ui.html b/inpage-toolbar-ui/toolbar/ui.html
deleted file mode 100644
index 981bcf3..0000000
--- a/inpage-toolbar-ui/toolbar/ui.html
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- In-Page "Amazing and super-useful" Toolbar
-
-
-
-
-
diff --git a/inpage-toolbar-ui/toolbar/ui.js b/inpage-toolbar-ui/toolbar/ui.js
deleted file mode 100644
index 4d67626..0000000
--- a/inpage-toolbar-ui/toolbar/ui.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Connect to the background page.
-var port = chrome.runtime.connect();
-
-// Handle click events on the toolbar button.
-document.querySelector("#toggle").addEventListener("click", function() {
- // Ask the background page to toggle the toolbar on the current tab
- port.postMessage("toggle-in-page-toolbar");
-});
diff --git a/inpage-toolbar-ui/toolbar/whimsy.png b/inpage-toolbar-ui/toolbar/whimsy.png
deleted file mode 100644
index 261ee19..0000000
Binary files a/inpage-toolbar-ui/toolbar/whimsy.png and /dev/null differ
diff --git a/latest-download/manifest.json b/latest-download/manifest.json
index efe4e1f..e240f2e 100644
--- a/latest-download/manifest.json
+++ b/latest-download/manifest.json
@@ -9,13 +9,6 @@
"48": "icons/page-48.png"
},
- "applications": {
- "gecko": {
- "id": "latest-download@mozilla.org",
- "strict_min_version": "48.0a1"
- }
- },
-
"permissions": [
"downloads",
"downloads.open"
diff --git a/latest-download/popup/latest_download.js b/latest-download/popup/latest_download.js
index 5423681..5619131 100644
--- a/latest-download/popup/latest_download.js
+++ b/latest-download/popup/latest_download.js
@@ -3,22 +3,17 @@ var latestDownloadId;
/*
Callback from getFileIcon.
-Log an error, or initialize the displayed icon.
+Initialize the displayed icon.
*/
function updateIconUrl(iconUrl) {
- /*
- If there was an error getting the icon URL,
- then lastError will be set. So check lastError
- and handle it.
- */
- if (chrome.runtime.lastError) {
- console.error(chrome.runtime.lastError);
- iconUrl = "";
- }
var downloadIcon = document.querySelector("#icon");
downloadIcon.setAttribute("src", iconUrl);
}
+function onError(error) {
+ console.log(`Error: ${error}`);
+}
+
/*
If there was a download item,
- remember its ID as latestDownloadId
@@ -30,7 +25,8 @@ function initializeLatestDownload(downloadItems) {
var downloadUrl = document.querySelector("#url");
if (downloadItems.length > 0) {
latestDownloadId = downloadItems[0].id;
- chrome.downloads.getFileIcon(latestDownloadId, updateIconUrl);
+ var gettingIconUrl = browser.downloads.getFileIcon(latestDownloadId);
+ gettingIconUrl.then(updateIconUrl, onError);
downloadUrl.textContent = downloadItems[0].url;
document.querySelector("#open").classList.remove("disabled");
document.querySelector("#remove").classList.remove("disabled");
@@ -44,17 +40,18 @@ function initializeLatestDownload(downloadItems) {
/*
Search for the most recent download, and pass it to initializeLatestDownload()
*/
-chrome.downloads.search({
+var searching = browser.downloads.search({
limit: 1,
orderBy: ["-startTime"]
-}, initializeLatestDownload);
+});
+searching.then(initializeLatestDownload);
/*
Open the item using the associated application.
*/
function openItem() {
if (!document.querySelector("#open").classList.contains("disabled")) {
- chrome.downloads.open(latestDownloadId);
+ browser.downloads.open(latestDownloadId);
}
}
@@ -63,8 +60,8 @@ Remove item from disk (removeFile) and from the download history (erase)
*/
function removeItem() {
if (!document.querySelector("#remove").classList.contains("disabled")) {
- chrome.downloads.removeFile(latestDownloadId);
- chrome.downloads.erase({id: latestDownloadId});
+ browser.downloads.removeFile(latestDownloadId);
+ browser.downloads.erase({id: latestDownloadId});
window.close();
}
}
diff --git a/list-cookies/README.md b/list-cookies/README.md
new file mode 100644
index 0000000..877c00e
--- /dev/null
+++ b/list-cookies/README.md
@@ -0,0 +1,9 @@
+# list-cookies
+
+## What it does
+
+This extensions list the cookies in the active tab.
+
+# What it shows
+
+Demonstration of the getAll() function in the cookie API
diff --git a/list-cookies/cookies.css b/list-cookies/cookies.css
new file mode 100644
index 0000000..95af25c
--- /dev/null
+++ b/list-cookies/cookies.css
@@ -0,0 +1,11 @@
+html, body {
+ width: 500px;
+}
+
+.panel {
+ padding: 5px;
+}
+
+li {
+ margin-bottom: 5px;
+}
diff --git a/list-cookies/cookies.html b/list-cookies/cookies.html
new file mode 100644
index 0000000..a15ad76
--- /dev/null
+++ b/list-cookies/cookies.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/list-cookies/cookies.js b/list-cookies/cookies.js
new file mode 100644
index 0000000..db2fc8e
--- /dev/null
+++ b/list-cookies/cookies.js
@@ -0,0 +1,39 @@
+function showCookiesForTab(tabs) {
+ //get the first tab object in the array
+ let tab = tabs.pop();
+
+ //get all cookies in the domain
+ var gettingAllCookies = browser.cookies.getAll({url: tab.url});
+ gettingAllCookies.then((cookies) => {
+
+ //set the header of the panel
+ var activeTabUrl = document.getElementById('header-title');
+ var text = document.createTextNode("Cookies at: "+tab.title);
+ var cookieList = document.getElementById('cookie-list');
+ activeTabUrl.appendChild(text);
+
+ if (cookies.length > 0) {
+ //add an
item with the name and value of the cookie to the list
+ 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 {
+ 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
+function getActiveTab() {
+ return browser.tabs.query({currentWindow: true, active: true});
+}
+getActiveTab().then(showCookiesForTab);
diff --git a/list-cookies/icons/cookie.png b/list-cookies/icons/cookie.png
new file mode 100644
index 0000000..5567982
Binary files /dev/null and b/list-cookies/icons/cookie.png differ
diff --git a/list-cookies/icons/cookie@2x.png b/list-cookies/icons/cookie@2x.png
new file mode 100644
index 0000000..d3de8e9
Binary files /dev/null and b/list-cookies/icons/cookie@2x.png differ
diff --git a/list-cookies/icons/default19.png b/list-cookies/icons/default19.png
new file mode 100644
index 0000000..aa4d283
Binary files /dev/null and b/list-cookies/icons/default19.png differ
diff --git a/list-cookies/icons/default38.png b/list-cookies/icons/default38.png
new file mode 100644
index 0000000..335fe0a
Binary files /dev/null and b/list-cookies/icons/default38.png differ
diff --git a/list-cookies/manifest.json b/list-cookies/manifest.json
new file mode 100644
index 0000000..9dbec54
--- /dev/null
+++ b/list-cookies/manifest.json
@@ -0,0 +1,21 @@
+{
+ "browser_action": {
+ "browser_style": true,
+ "default_title": "List cookies in the active tab",
+ "default_popup": "cookies.html",
+ "default_icon": {
+ "19": "icons/default19.png",
+ "38": "icons/default38.png"
+ }
+ },
+ "description": "List cookies in the active tab.",
+ "icons": {
+ "48": "icons/cookie.png",
+ "96": "icons/cookie@2x.png"
+ },
+ "homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/list-cookies",
+ "manifest_version": 2,
+ "name": "List cookies",
+ "version": "1.0",
+ "permissions": ["cookies","","tabs"]
+}
diff --git a/menu-demo/README.md b/menu-demo/README.md
new file mode 100644
index 0000000..ca79920
--- /dev/null
+++ b/menu-demo/README.md
@@ -0,0 +1,36 @@
+# menu-demo
+
+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:
+
+* one shown when there is a selection in the page, that logs the selected text
+to the browser console when clicked.
+* one shown in all contexts, that is removed when clicked.
+* two "radio" items that are shown in all contexts.
+These items are grouped using a separator item on each side.
+One radio item adds a blue border to the page, the other adds a green border.
+Note that these buttons only work on normal web pages, not special pages
+like about:debugging.
+* one "checkbox" item, shown in all contexts, whose title is updated when the
+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 menu item:
+ * normal
+ * radio
+ * separator
+ * checkbox
+* How to use contexts to control when an item appears.
+* How to update an item's properties.
+* How to remove an item.
diff --git a/context-menu-demo/_locales/en/messages.json b/menu-demo/_locales/en/messages.json
similarity index 65%
rename from context-menu-demo/_locales/en/messages.json
rename to menu-demo/_locales/en/messages.json
index d4022f3..709898f 100644
--- a/context-menu-demo/_locales/en/messages.json
+++ b/menu-demo/_locales/en/messages.json
@@ -1,42 +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."
+ },
+
+ "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 50%
rename from context-menu-demo/background.js
rename to menu-demo/background.js
index cac9325..2a32f38 100644
--- a/context-menu-demo/background.js
+++ b/menu-demo/background.js
@@ -2,64 +2,76 @@
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) {
- if (chrome.runtime.lastError) {
- console.log("error creating item:" + chrome.runtime.lastError);
+function onCreated() {
+ if (browser.runtime.lastError) {
+ console.log(`Error: ${browser.runtime.lastError}`);
} else {
- console.log("item created successfully");
+ console.log("Item created successfully");
}
}
/*
-Called when the item has been removed, or when there was an error.
-We'll just log success or failure here.
+Called when the item has been removed.
+We'll just log success here.
*/
function onRemoved() {
- if (chrome.runtime.lastError) {
- console.log("error removing item:" + chrome.runtime.lastError);
- } else {
- console.log("item removed successfully");
- }
+ console.log("Item removed successfully");
+}
+
+/*
+Called when there was an error.
+We'll just log the error here.
+*/
+function onError(error) {
+ console.log(`Error: ${error}`);
}
/*
Create all the context menu items.
*/
-chrome.contextMenus.create({
+browser.menus.create({
id: "log-selection",
- title: chrome.i18n.getMessage("contextMenuItemSelectionLogger"),
+ title: browser.i18n.getMessage("menuItemSelectionLogger"),
contexts: ["selection"]
}, onCreated);
-chrome.contextMenus.create({
+browser.menus.create({
id: "remove-me",
- title: chrome.i18n.getMessage("contextMenuItemRemoveMe"),
+ title: browser.i18n.getMessage("menuItemRemoveMe"),
contexts: ["all"]
}, onCreated);
-chrome.contextMenus.create({
+browser.menus.create({
id: "separator-1",
type: "separator",
contexts: ["all"]
}, onCreated);
-chrome.contextMenus.create({
+browser.menus.create({
id: "greenify",
type: "radio",
- title: chrome.i18n.getMessage("contextMenuItemGreenify"),
+ title: browser.i18n.getMessage("menuItemGreenify"),
contexts: ["all"],
- checked: true
+ checked: true,
+ icons: {
+ "16": "icons/paint-green-16.png",
+ "32": "icons/paint-green-32.png"
+ }
}, onCreated);
-chrome.contextMenus.create({
+browser.menus.create({
id: "bluify",
type: "radio",
- title: chrome.i18n.getMessage("contextMenuItemBluify"),
+ title: browser.i18n.getMessage("menuItemBluify"),
contexts: ["all"],
- checked: false
+ checked: false,
+ icons: {
+ "16": "icons/paint-blue-16.png",
+ "32": "icons/paint-blue-32.png"
+ }
}, onCreated);
-chrome.contextMenus.create({
+browser.menus.create({
id: "separator-2",
type: "separator",
contexts: ["all"]
@@ -67,14 +79,27 @@ chrome.contextMenus.create({
var checkedState = true;
-chrome.contextMenus.create({
+browser.menus.create({
id: "check-uncheck",
type: "checkbox",
- title: chrome.i18n.getMessage("contextMenuItemUncheckMe"),
+ title: browser.i18n.getMessage("menuItemUncheckMe"),
contexts: ["all"],
checked: checkedState
}, onCreated);
+browser.menus.create({
+ id: "open-sidebar",
+ 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.
@@ -85,7 +110,7 @@ var blue = 'document.body.style.border = "5px solid blue"';
var green = 'document.body.style.border = "5px solid green"';
function borderify(tabId, color) {
- chrome.tabs.executeScript(tabId, {
+ browser.tabs.executeScript(tabId, {
code: color
});
}
@@ -101,12 +126,12 @@ property into the event listener.
function updateCheckUncheck() {
checkedState = !checkedState;
if (checkedState) {
- chrome.contextMenus.update("check-uncheck", {
- title: chrome.i18n.getMessage("contextMenuItemUncheckMe"),
+ browser.menus.update("check-uncheck", {
+ title: browser.i18n.getMessage("menuItemUncheckMe"),
});
} else {
- chrome.contextMenus.update("check-uncheck", {
- title: chrome.i18n.getMessage("contextMenuItemCheckMe"),
+ browser.menus.update("check-uncheck", {
+ title: browser.i18n.getMessage("menuItemCheckMe"),
});
}
}
@@ -115,13 +140,14 @@ function updateCheckUncheck() {
The click event listener, where we perform the appropriate action given the
ID of the menu item that was clicked.
*/
-chrome.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":
- chrome.contextMenus.remove(info.menuItemId, onRemoved);
+ var removing = browser.menus.remove(info.menuItemId);
+ removing.then(onRemoved, onError);
break;
case "bluify":
borderify(tab.id, blue);
@@ -132,5 +158,11 @@ chrome.contextMenus.onClicked.addListener(function(info, tab) {
case "check-uncheck":
updateCheckUncheck();
break;
+ case "open-sidebar":
+ console.log("Opening my sidebar");
+ break;
+ case "tools-menu":
+ console.log("Clicked the tools menu item");
+ break;
}
});
diff --git a/menu-demo/icons/LICENSE b/menu-demo/icons/LICENSE
new file mode 100644
index 0000000..5e98d3b
--- /dev/null
+++ b/menu-demo/icons/LICENSE
@@ -0,0 +1,4 @@
+
+The "page-32.png" and "page-48.png" icons are taken from the miu iconset created by Linh Pham Thi Dieu, and are used under the terms of its license: http://linhpham.me/miu/.
+
+The "paint-blue-16", "paint-blue-32", "paint-green-16", and "paint-green-32" icons are adapted from an icon in the ["Outline icons" set](https://www.iconfinder.com/icons/1021026/paint_icon).
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/menu-demo/icons/page-32.png b/menu-demo/icons/page-32.png
new file mode 100644
index 0000000..dae663d
Binary files /dev/null and b/menu-demo/icons/page-32.png differ
diff --git a/menu-demo/icons/page-48.png b/menu-demo/icons/page-48.png
new file mode 100644
index 0000000..ba042cd
Binary files /dev/null and b/menu-demo/icons/page-48.png differ
diff --git a/menu-demo/icons/paint-blue-16.png b/menu-demo/icons/paint-blue-16.png
new file mode 100644
index 0000000..24f9c73
Binary files /dev/null and b/menu-demo/icons/paint-blue-16.png differ
diff --git a/menu-demo/icons/paint-blue-32.png b/menu-demo/icons/paint-blue-32.png
new file mode 100644
index 0000000..8b001b8
Binary files /dev/null and b/menu-demo/icons/paint-blue-32.png differ
diff --git a/menu-demo/icons/paint-green-16.png b/menu-demo/icons/paint-green-16.png
new file mode 100644
index 0000000..02f35a8
Binary files /dev/null and b/menu-demo/icons/paint-green-16.png differ
diff --git a/menu-demo/icons/paint-green-32.png b/menu-demo/icons/paint-green-32.png
new file mode 100644
index 0000000..eb940a6
Binary files /dev/null and b/menu-demo/icons/paint-green-32.png differ
diff --git a/context-menu-demo/manifest.json b/menu-demo/manifest.json
similarity index 54%
rename from context-menu-demo/manifest.json
rename to menu-demo/manifest.json
index aba22a3..f984c05 100644
--- a/context-menu-demo/manifest.json
+++ b/menu-demo/manifest.json
@@ -1,14 +1,13 @@
{
"manifest_version": 2,
- "name": "context-menu-demo",
+ "name": "__MSG_extensionName__",
+ "description": "__MSG_extensionDescription__",
"version": "1.0",
"default_locale": "en",
-
"applications": {
"gecko": {
- "id": "context-menu-demo@mozilla.org",
- "strict_min_version": "48.0"
+ "strict_min_version": "56.0a1"
}
},
@@ -17,7 +16,7 @@
},
"permissions": [
- "contextMenus",
+ "menus",
"activeTab"
],
@@ -25,6 +24,12 @@
"16": "icons/page-16.png",
"32": "icons/page-32.png",
"48": "icons/page-48.png"
+ },
+
+ "sidebar_action": {
+ "default_icon": "icons/page-32.png",
+ "default_title" : "My sidebar",
+ "default_panel": "sidebar/sidebar.html"
}
}
diff --git a/menu-demo/sidebar/sidebar.html b/menu-demo/sidebar/sidebar.html
new file mode 100644
index 0000000..bbfc03b
--- /dev/null
+++ b/menu-demo/sidebar/sidebar.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
My panel
+
+
+
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/.gitignore b/mocha-client-tests/.gitignore
new file mode 100644
index 0000000..f53b1be
--- /dev/null
+++ b/mocha-client-tests/.gitignore
@@ -0,0 +1,4 @@
+node_modules
+addon/bower_components
+addon/node_modules
+.idea
\ No newline at end of file
diff --git a/mocha-client-tests/README.md b/mocha-client-tests/README.md
new file mode 100644
index 0000000..d66c3b4
--- /dev/null
+++ b/mocha-client-tests/README.md
@@ -0,0 +1,40 @@
+# Mocha client tests for WebExtensions
+## Introduction
+This example shows two methods of testing a WebExtension:
+* Running tests from within the addon
+* Running tests from the commandline using Karma
+
+See https://github.com/Standard8/example-webextension for a more complete example of WebExtension test configuration.
+
+## Install Dependencies:
+```
+ npm install
+```
+To run tests from within the addon:
+```
+ cd addon
+ npm install
+```
+
+## Testing within the Addon
+This gives you the possibility to run client tests inside the addon with the mocha UI.
+If you don't want to use the mocha UI, you can install [WebConsole-reporter](https://github.com/eeroan/WebConsole-reporter).
+
+### Run with web-ext cli
+Just run `npm run web-ext` (will work with FF dev edition), if you have error with web-ext cli please add path for FF binary file with `--firefox-binary /path/to/firefox-bin`
+[(web-ext docs)](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/web-ext_command_reference).
+
+When the addon starts, click on the mocha icon in your browser bar to run the tests:
+
+
+
+This will test `./addon/background.js` with `./addon/tests/lib/background-messaging.test.js`.
+
+## Testing from the Commandline
+This uses [Karma](http://karma-runner.github.io) to run tests from the commandline. Just type `npm test` to test `./addon/background.js` with `./tests/lib/background.test.js`.
+
+### Debug Mode
+Use `npm run test:debug` to run Karma in watch mode. Whenever you modify a Javascript file, the tests will automatically rerun.
+
+You can install [karma-notification-reporter](https://www.npmjs.com/package/karma-notification-reporter) to display test results in a desktop notification. You'll need to add `--reporters=dots,notification` to the `test:debug` command line of
+`package.json` to enable it.
diff --git a/mocha-client-tests/addon/background.js b/mocha-client-tests/addon/background.js
new file mode 100644
index 0000000..f356388
--- /dev/null
+++ b/mocha-client-tests/addon/background.js
@@ -0,0 +1,15 @@
+var Background = {
+ receiveMessage: function(msg, sender, sendResponse) {
+ if (msg && msg.action && Background.hasOwnProperty(msg.action)) {
+ return Background[msg.action](msg, sender, sendResponse);
+ } else {
+ console.warn('No handler for message: ' + JSON.stringify(msg));
+ }
+ },
+ ping: function(msg, sender, sendResponse) {
+ sendResponse('pong');
+ return true;
+ }
+};
+
+browser.runtime.onMessage.addListener(Background.receiveMessage);
diff --git a/mocha-client-tests/addon/images/icon-16.png b/mocha-client-tests/addon/images/icon-16.png
new file mode 100644
index 0000000..01da9f4
Binary files /dev/null and b/mocha-client-tests/addon/images/icon-16.png differ
diff --git a/mocha-client-tests/addon/images/icon-19.png b/mocha-client-tests/addon/images/icon-19.png
new file mode 100644
index 0000000..1c80c9d
Binary files /dev/null and b/mocha-client-tests/addon/images/icon-19.png differ
diff --git a/mocha-client-tests/addon/images/mocha.png b/mocha-client-tests/addon/images/mocha.png
new file mode 100644
index 0000000..83f1196
Binary files /dev/null and b/mocha-client-tests/addon/images/mocha.png differ
diff --git a/mocha-client-tests/addon/manifest.json b/mocha-client-tests/addon/manifest.json
new file mode 100644
index 0000000..26b0f43
--- /dev/null
+++ b/mocha-client-tests/addon/manifest.json
@@ -0,0 +1,28 @@
+{
+ "name": "Mocha tests",
+ "version": "1.0",
+ "manifest_version": 2,
+ "description": "Check ",
+ "icons": {
+ "16": "images/icon-16.png"
+ },
+ "short_name": "MochaTest",
+ "background": {
+ "scripts": [
+ "background.js"
+ ]
+ },
+ "browser_action": {
+ "default_icon": {
+ "19": "images/icon-19.png"
+ },
+ "default_title": "Mocha Test",
+ "default_popup": "popup.html"
+ },
+ "applications": {
+ "gecko": {
+ "strict_min_version": "45.0"
+ }
+ },
+ "content_security_policy": "script-src 'self'; object-src 'self'; img-src 'self'"
+}
diff --git a/mocha-client-tests/addon/package.json b/mocha-client-tests/addon/package.json
new file mode 100644
index 0000000..31faea1
--- /dev/null
+++ b/mocha-client-tests/addon/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "mocha-tests-webextension",
+ "version": "1.0.0",
+ "description": "Run test inside your addon",
+ "main": "background.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "",
+ "license": "MPL-2.0",
+ "devDependencies": {
+ "expect.js": "^0.3.1",
+ "mocha": "^3.1.2"
+ }
+}
diff --git a/mocha-client-tests/addon/popup.html b/mocha-client-tests/addon/popup.html
new file mode 100644
index 0000000..9c33a6f
--- /dev/null
+++ b/mocha-client-tests/addon/popup.html
@@ -0,0 +1,25 @@
+
+
+
+ Mocha Tests
+
+
+
+
+
+
+
+
+
Hello! Lets play at ping
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mocha-client-tests/addon/scripts/browser-polyfill.min.js b/mocha-client-tests/addon/scripts/browser-polyfill.min.js
new file mode 100644
index 0000000..8c87af7
--- /dev/null
+++ b/mocha-client-tests/addon/scripts/browser-polyfill.min.js
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+if(typeof browser==="undefined"){const wrapAPIs=()=>{const apiMetadata={"alarms":{"clear":{"minArgs":0,"maxArgs":1},"clearAll":{"minArgs":0,"maxArgs":0},"get":{"minArgs":0,"maxArgs":1},"getAll":{"minArgs":0,"maxArgs":0}},"bookmarks":{"create":{"minArgs":1,"maxArgs":1},"export":{"minArgs":0,"maxArgs":0},"get":{"minArgs":1,"maxArgs":1},"getChildren":{"minArgs":1,"maxArgs":1},"getRecent":{"minArgs":1,"maxArgs":1},"getTree":{"minArgs":0,"maxArgs":0},"getSubTree":{"minArgs":1,"maxArgs":1},"import":{"minArgs":0,
+"maxArgs":0},"move":{"minArgs":2,"maxArgs":2},"remove":{"minArgs":1,"maxArgs":1},"removeTree":{"minArgs":1,"maxArgs":1},"search":{"minArgs":1,"maxArgs":1},"update":{"minArgs":2,"maxArgs":2}},"browserAction":{"getBadgeBackgroundColor":{"minArgs":1,"maxArgs":1},"getBadgeText":{"minArgs":1,"maxArgs":1},"getPopup":{"minArgs":1,"maxArgs":1},"getTitle":{"minArgs":1,"maxArgs":1},"setIcon":{"minArgs":1,"maxArgs":1}},"commands":{"getAll":{"minArgs":0,"maxArgs":0}},"contextMenus":{"update":{"minArgs":2,"maxArgs":2},
+"remove":{"minArgs":1,"maxArgs":1},"removeAll":{"minArgs":0,"maxArgs":0}},"cookies":{"get":{"minArgs":1,"maxArgs":1},"getAll":{"minArgs":1,"maxArgs":1},"getAllCookieStores":{"minArgs":0,"maxArgs":0},"remove":{"minArgs":1,"maxArgs":1},"set":{"minArgs":1,"maxArgs":1}},"downloads":{"download":{"minArgs":1,"maxArgs":1},"cancel":{"minArgs":1,"maxArgs":1},"erase":{"minArgs":1,"maxArgs":1},"getFileIcon":{"minArgs":1,"maxArgs":2},"open":{"minArgs":1,"maxArgs":1},"pause":{"minArgs":1,"maxArgs":1},"removeFile":{"minArgs":1,
+"maxArgs":1},"resume":{"minArgs":1,"maxArgs":1},"search":{"minArgs":1,"maxArgs":1},"show":{"minArgs":1,"maxArgs":1}},"extension":{"isAllowedFileSchemeAccess":{"minArgs":0,"maxArgs":0},"isAllowedIncognitoAccess":{"minArgs":0,"maxArgs":0}},"history":{"addUrl":{"minArgs":1,"maxArgs":1},"getVisits":{"minArgs":1,"maxArgs":1},"deleteAll":{"minArgs":0,"maxArgs":0},"deleteRange":{"minArgs":1,"maxArgs":1},"deleteUrl":{"minArgs":1,"maxArgs":1},"search":{"minArgs":1,"maxArgs":1}},"i18n":{"detectLanguage":{"minArgs":1,
+"maxArgs":1},"getAcceptLanguages":{"minArgs":0,"maxArgs":0}},"idle":{"queryState":{"minArgs":1,"maxArgs":1}},"management":{"get":{"minArgs":1,"maxArgs":1},"getAll":{"minArgs":0,"maxArgs":0},"getSelf":{"minArgs":0,"maxArgs":0},"uninstallSelf":{"minArgs":0,"maxArgs":1}},"notifications":{"clear":{"minArgs":1,"maxArgs":1},"create":{"minArgs":1,"maxArgs":2},"getAll":{"minArgs":0,"maxArgs":0},"getPermissionLevel":{"minArgs":0,"maxArgs":0},"update":{"minArgs":2,"maxArgs":2}},"pageAction":{"getPopup":{"minArgs":1,
+"maxArgs":1},"getTitle":{"minArgs":1,"maxArgs":1},"hide":{"minArgs":0,"maxArgs":0},"setIcon":{"minArgs":1,"maxArgs":1},"show":{"minArgs":0,"maxArgs":0}},"runtime":{"getBackgroundPage":{"minArgs":0,"maxArgs":0},"getBrowserInfo":{"minArgs":0,"maxArgs":0},"getPlatformInfo":{"minArgs":0,"maxArgs":0},"openOptionsPage":{"minArgs":0,"maxArgs":0},"requestUpdateCheck":{"minArgs":0,"maxArgs":0},"sendMessage":{"minArgs":1,"maxArgs":3},"sendNativeMessage":{"minArgs":2,"maxArgs":2},"setUninstallURL":{"minArgs":1,
+"maxArgs":1}},"storage":{"local":{"clear":{"minArgs":0,"maxArgs":0},"get":{"minArgs":0,"maxArgs":1},"getBytesInUse":{"minArgs":0,"maxArgs":1},"remove":{"minArgs":1,"maxArgs":1},"set":{"minArgs":1,"maxArgs":1}},"managed":{"get":{"minArgs":0,"maxArgs":1},"getBytesInUse":{"minArgs":0,"maxArgs":1}},"sync":{"clear":{"minArgs":0,"maxArgs":0},"get":{"minArgs":0,"maxArgs":1},"getBytesInUse":{"minArgs":0,"maxArgs":1},"remove":{"minArgs":1,"maxArgs":1},"set":{"minArgs":1,"maxArgs":1}}},"tabs":{"create":{"minArgs":1,
+"maxArgs":1},"captureVisibleTab":{"minArgs":0,"maxArgs":2},"detectLanguage":{"minArgs":0,"maxArgs":1},"duplicate":{"minArgs":1,"maxArgs":1},"executeScript":{"minArgs":1,"maxArgs":2},"get":{"minArgs":1,"maxArgs":1},"getCurrent":{"minArgs":0,"maxArgs":0},"getZoom":{"minArgs":0,"maxArgs":1},"getZoomSettings":{"minArgs":0,"maxArgs":1},"highlight":{"minArgs":1,"maxArgs":1},"insertCSS":{"minArgs":1,"maxArgs":2},"move":{"minArgs":2,"maxArgs":2},"reload":{"minArgs":0,"maxArgs":2},"remove":{"minArgs":1,"maxArgs":1},
+"query":{"minArgs":1,"maxArgs":1},"removeCSS":{"minArgs":1,"maxArgs":2},"sendMessage":{"minArgs":2,"maxArgs":3},"setZoom":{"minArgs":1,"maxArgs":2},"setZoomSettings":{"minArgs":1,"maxArgs":2},"update":{"minArgs":1,"maxArgs":2}},"webNavigation":{"getAllFrames":{"minArgs":1,"maxArgs":1},"getFrame":{"minArgs":1,"maxArgs":1}},"webRequest":{"handlerBehaviorChanged":{"minArgs":0,"maxArgs":0}},"windows":{"create":{"minArgs":0,"maxArgs":1},"get":{"minArgs":1,"maxArgs":2},"getAll":{"minArgs":0,"maxArgs":1},
+"getCurrent":{"minArgs":0,"maxArgs":1},"getLastFocused":{"minArgs":0,"maxArgs":1},"remove":{"minArgs":1,"maxArgs":1},"update":{"minArgs":2,"maxArgs":2}}};class DefaultWeakMap extends WeakMap{constructor(createItem,items=undefined){super(items);this.createItem=createItem}get(key){if(!this.has(key))this.set(key,this.createItem(key));return super.get(key)}}const isThenable=(value)=>{return value&&typeof value==="object"&&typeof value.then==="function"};const makeCallback=(promise)=>{return(...callbackArgs)=>
+{if(chrome.runtime.lastError)promise.reject(chrome.runtime.lastError);else if(callbackArgs.length===1)promise.resolve(callbackArgs[0]);else promise.resolve(callbackArgs)}};const wrapAsyncFunction=(name,metadata)=>{return function asyncFunctionWrapper(target,...args){if(args.lengthmetadata.maxArgs)throw new Error(`Expected at most ${metadata.maxArgs} arguments for ${name}(), got ${args.length}`);
+return new Promise((resolve,reject)=>{target[name](...args,makeCallback({resolve,reject}))})}};const wrapMethod=(target,method,wrapper)=>{return new Proxy(method,{apply(targetMethod,thisObj,args){return wrapper.call(thisObj,target,...args)}})};let hasOwnProperty=Function.call.bind(Object.prototype.hasOwnProperty);const wrapObject=(target,wrappers={},metadata={})=>{let cache=Object.create(null);let handlers={has(target,prop){return prop in target||prop in cache},get(target,prop,receiver){if(prop in
+cache)return cache[prop];if(!(prop in target))return undefined;let value=target[prop];if(typeof value==="function")if(typeof wrappers[prop]==="function")value=wrapMethod(target,target[prop],wrappers[prop]);else if(hasOwnProperty(metadata,prop)){let wrapper=wrapAsyncFunction(prop,metadata[prop]);value=wrapMethod(target,target[prop],wrapper)}else value=value.bind(target);else if(typeof value==="object"&&value!==null&&(hasOwnProperty(wrappers,prop)||hasOwnProperty(metadata,prop)))value=wrapObject(value,
+wrappers[prop],metadata[prop]);else{Object.defineProperty(cache,prop,{configurable:true,enumerable:true,get(){return target[prop]},set(value){target[prop]=value}});return value}cache[prop]=value;return value},set(target,prop,value,receiver){if(prop in cache)cache[prop]=value;else target[prop]=value;return true},defineProperty(target,prop,desc){return Reflect.defineProperty(cache,prop,desc)},deleteProperty(target,prop){return Reflect.deleteProperty(cache,prop)}};return new Proxy(target,handlers)};
+const wrapEvent=(wrapperMap)=>{addListener(target,listener,...args){target.addListener(wrapperMap.get(listener),...args)},hasListener(target,listener){return target.hasListener(wrapperMap.get(listener))},removeListener(target,listener){target.removeListener(wrapperMap.get(listener))}};const onMessageWrappers=new DefaultWeakMap((listener)=>{if(typeof listener!=="function")return listener;return function onMessage(message,sender,sendResponse){let result=listener(message,sender);if(isThenable(result)){result.then(sendResponse,
+(error)=>{console.error(error);sendResponse(error)});return true}else if(result!==undefined)sendResponse(result)}});const staticWrappers={runtime:{onMessage:wrapEvent(onMessageWrappers)}};return wrapObject(chrome,staticWrappers,apiMetadata)};this.browser=wrapAPIs()};
diff --git a/mocha-client-tests/addon/scripts/popup.js b/mocha-client-tests/addon/scripts/popup.js
new file mode 100644
index 0000000..9e32f28
--- /dev/null
+++ b/mocha-client-tests/addon/scripts/popup.js
@@ -0,0 +1,10 @@
+ setInterval(function() {
+ var $game = document.querySelector('#game');
+ if($game.innerText !== 'ping'){
+ $game.innerText = 'ping';
+ } else{
+ browser.runtime.sendMessage({action: 'ping'}).then((response) => {
+ $game.innerText = response;
+ });
+ }
+ }, 1000);
diff --git a/mocha-client-tests/addon/tests/lib/background-messaging.test.js b/mocha-client-tests/addon/tests/lib/background-messaging.test.js
new file mode 100644
index 0000000..b91eb5a
--- /dev/null
+++ b/mocha-client-tests/addon/tests/lib/background-messaging.test.js
@@ -0,0 +1,11 @@
+describe('Background', function() {
+ describe('ping', function() {
+ it('should return pong in response', function() {
+ // Return a promise for Mocha using the Firefox browser API instead of chrome.
+ return browser.runtime.sendMessage({action: 'ping'})
+ .then(function(response) {
+ expect(response).to.equal('pong');
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/mocha-client-tests/addon/tests/lib/test.array.js b/mocha-client-tests/addon/tests/lib/test.array.js
new file mode 100644
index 0000000..40a5f5e
--- /dev/null
+++ b/mocha-client-tests/addon/tests/lib/test.array.js
@@ -0,0 +1,8 @@
+describe('Array', function() {
+ describe('#indexOf()', function() {
+ it('should return -1 when the value is not present', function() {
+ expect([1,2,3]).to.not.contain(5);
+ expect([1,2,3]).to.not.contain(0);
+ });
+ });
+});
\ No newline at end of file
diff --git a/mocha-client-tests/addon/tests/mocha-run.js b/mocha-client-tests/addon/tests/mocha-run.js
new file mode 100644
index 0000000..ec1be52
--- /dev/null
+++ b/mocha-client-tests/addon/tests/mocha-run.js
@@ -0,0 +1,5 @@
+mocha.checkLeaks();
+// Here we add initial global variables to prevent this error:
+// Error: global leaks detected: AppView, ExtensionOptions, ExtensionView, WebView
+mocha.globals(['AppView', 'ExtensionOptions', 'ExtensionView', 'WebView']);
+mocha.run();
\ No newline at end of file
diff --git a/mocha-client-tests/addon/tests/mocha-setup.js b/mocha-client-tests/addon/tests/mocha-setup.js
new file mode 100644
index 0000000..58a9e9e
--- /dev/null
+++ b/mocha-client-tests/addon/tests/mocha-setup.js
@@ -0,0 +1 @@
+mocha.setup('bdd');
\ No newline at end of file
diff --git a/mocha-client-tests/karma.conf.js b/mocha-client-tests/karma.conf.js
new file mode 100644
index 0000000..9544b1d
--- /dev/null
+++ b/mocha-client-tests/karma.conf.js
@@ -0,0 +1,73 @@
+module.exports = function(config) {
+ config.set({
+
+ // base path that will be used to resolve all patterns (eg. files, exclude)
+ basePath: '',
+
+ // frameworks to use
+ // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
+ frameworks: ['mocha'],
+
+ // list of files / patterns to load in the browser
+ files: [
+ // Test dependencies
+ 'node_modules/expect.js/index.js',
+ 'node_modules/sinon-chrome/bundle/sinon-chrome-webextensions.min.js',
+
+ // Source
+ 'addon/*.js',
+
+ // Tests
+ 'tests/lib/*.js'
+ ],
+
+ // The tests below are intended to be run from inside the WebExtension itself,
+ // not from the Karma test suite.
+ exclude: [
+ 'addon/tests',
+ ],
+
+ client: {
+ mocha: {
+ // change Karma's debug.html to the mocha web reporter
+ reporter: 'html',
+ },
+ },
+
+ // preprocess matching files before serving them to the browser
+ // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
+ preprocessors: {
+ },
+
+ // test results reporter to use
+ // possible values: 'dots', 'progress'
+ // available reporters: https://npmjs.org/browse/keyword/karma-reporter
+ reporters: ['dots'],
+
+ // web server port
+ port: 9876,
+
+ // enable / disable colors in the output (reporters and logs)
+ colors: true,
+
+ // level of logging
+ // possible values: config.LOG_DISABLE || config.LOG_ERROR ||
+ // config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+ logLevel: config.LOG_INFO,
+
+ // enable/disable watching file and executing tests when any file changes
+ autoWatch: false,
+
+ // start these browsers
+ // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
+ browsers: ['Firefox'],
+
+ // Continuous Integration mode
+ // if true, Karma captures browsers, runs the tests and exits
+ singleRun: true,
+
+ // Concurrency level
+ // how many browser should be started simultaneous
+ concurrency: Infinity,
+ });
+};
diff --git a/mocha-client-tests/package.json b/mocha-client-tests/package.json
new file mode 100644
index 0000000..27976d1
--- /dev/null
+++ b/mocha-client-tests/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "mocha-test-webextension",
+ "version": "1.0.0",
+ "description": "Example how to run unit tests for WebExtension",
+ "main": "index.js",
+ "scripts": {
+ "test": "karma start",
+ "test:debug": "karma start --no-single-run --auto-watch",
+ "web-ext": "web-ext run -s ./addon"
+ },
+ "author": "",
+ "license": "MPL-2.0",
+ "devDependencies": {
+ "chai": "^3.5.0",
+ "expect.js": "^0.3.1",
+ "karma": "^1.3.0",
+ "karma-firefox-launcher": "^1.0.0",
+ "karma-mocha": "^1.3.0",
+ "mocha": "^3.1.2",
+ "sinon-chrome": "^2.1.2",
+ "web-ext": "^1.6.0"
+ }
+}
diff --git a/mocha-client-tests/screenshots/addon-button.png b/mocha-client-tests/screenshots/addon-button.png
new file mode 100644
index 0000000..6172e83
Binary files /dev/null and b/mocha-client-tests/screenshots/addon-button.png differ
diff --git a/mocha-client-tests/tests/lib/background.test.js b/mocha-client-tests/tests/lib/background.test.js
new file mode 100644
index 0000000..0588bb3
--- /dev/null
+++ b/mocha-client-tests/tests/lib/background.test.js
@@ -0,0 +1,10 @@
+describe('Background', function() {
+ describe('ping', function() {
+ it('should return pong in response', function(done) {
+ Background.ping(false, false, function(response) {
+ expect(response).to.equal('pong');
+ done();
+ });
+ });
+ });
+});
diff --git a/native-messaging/README.md b/native-messaging/README.md
new file mode 100644
index 0000000..b2c5999
--- /dev/null
+++ b/native-messaging/README.md
@@ -0,0 +1,33 @@
+This is a very simple example of how to use [native messaging](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Native_messaging) to exchange messages between a WebExtension and a native application.
+
+The WebExtension, which can be found under "add-on", connects to the native application and listens to messages from it. It then sends a message to the native application when the user clicks on the WebExtension's browser action. The message payload is just "ping".
+
+The native application, which can be found under "app", listens for messages from the WebExtension. When it receives a message, the native application sends a response message whose payload is just "pong". The native application is written in Python.
+
+## Setup ##
+
+To get this working, there's a little setup to do.
+
+### Mac OS/Linux setup ###
+
+1. Check that the [file permissions](https://en.wikipedia.org/wiki/File_system_permissions) for "ping_pong.py" include the `execute` permission.
+2. Edit the "path" property of "ping_pong.json" to point to the location of "ping_pong.py" on your computer.
+3. copy "ping_pong.json" to the correct location on your computer. See [App manifest location ](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Native_messaging#App_manifest_location) to find the correct location for your OS.
+
+### Windows setup ###
+
+1. Check you have Python installed, and that your system's PATH environment variable includes the path to Python. See [Using Python on Windows](https://docs.python.org/2/using/windows.html). You'll need to restart the web browser after making this change, or the browser won't pick up the new environment variable.
+2. Edit the "path" property of "ping_pong.json" to point to the location of "ping_pong_win.bat" on your computer. Note that you'll need to escape the Windows directory separator, like this: `"path": "C:\\Users\\MDN\\native-messaging\\app\\ping_pong_win.bat"`.
+3. Edit "ping_pong_win.bat" to refer to the location of "ping_pong.py" on your computer.
+4. Add a registry key containing the path to "ping_pong.json" on your computer. See [App manifest location ](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Native_messaging#App_manifest_location) to find details of the registry key to add.
+
+## Testing the example ##
+
+Then just install the add-on as usual, by visiting about:debugging, clicking "Load Temporary Add-on", and selecting the add-on's "manifest.json".
+
+You should see a new browser action icon in the toolbar. Open the console ("Tools/Web Developer/Browser Console" in Firefox), and click the browser action icon. You should see output like this in the console:
+
+ Sending: ping
+ Received: pong
+
+If you don't see this output, see the [Troubleshooting guide](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Native_messaging#Troubleshooting) for ideas.
diff --git a/native-messaging/add-on/background.js b/native-messaging/add-on/background.js
new file mode 100644
index 0000000..4cdabef
--- /dev/null
+++ b/native-messaging/add-on/background.js
@@ -0,0 +1,19 @@
+/*
+On startup, connect to the "ping_pong" app.
+*/
+var port = browser.runtime.connectNative("ping_pong");
+
+/*
+Listen for messages from the app.
+*/
+port.onMessage.addListener((response) => {
+ console.log("Received: " + response);
+});
+
+/*
+On a click on the browser action, send the app a message.
+*/
+browser.browserAction.onClicked.addListener(() => {
+ console.log("Sending: ping");
+ port.postMessage("ping");
+});
diff --git a/native-messaging/add-on/icons/LICENSE b/native-messaging/add-on/icons/LICENSE
new file mode 100644
index 0000000..c4e7bdc
--- /dev/null
+++ b/native-messaging/add-on/icons/LICENSE
@@ -0,0 +1 @@
+The icon used here is taken from the "Miscellany Web icons" set by Maria & Guillem (https://www.iconfinder.com/andromina), and is used under the Creative Commons (Attribution 3.0 Unported) license.
diff --git a/native-messaging/add-on/icons/message.svg b/native-messaging/add-on/icons/message.svg
new file mode 100644
index 0000000..7ee68e2
--- /dev/null
+++ b/native-messaging/add-on/icons/message.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/native-messaging/add-on/manifest.json b/native-messaging/add-on/manifest.json
new file mode 100644
index 0000000..bd3ba1f
--- /dev/null
+++ b/native-messaging/add-on/manifest.json
@@ -0,0 +1,28 @@
+{
+
+ "description": "Native messaging example add-on",
+ "manifest_version": 2,
+ "name": "Native messaging example",
+ "version": "1.0",
+ "icons": {
+ "48": "icons/message.svg"
+ },
+
+ "applications": {
+ "gecko": {
+ "id": "ping_pong@example.org",
+ "strict_min_version": "50.0"
+ }
+ },
+
+ "background": {
+ "scripts": ["background.js"]
+ },
+
+ "browser_action": {
+ "default_icon": "icons/message.svg"
+ },
+
+ "permissions": ["nativeMessaging"]
+
+}
diff --git a/native-messaging/app/ping_pong.json b/native-messaging/app/ping_pong.json
new file mode 100644
index 0000000..a257b18
--- /dev/null
+++ b/native-messaging/app/ping_pong.json
@@ -0,0 +1,7 @@
+{
+ "name": "ping_pong",
+ "description": "Example host for native messaging",
+ "path": "/path/to/native-messaging/app/ping_pong.py",
+ "type": "stdio",
+ "allowed_extensions": [ "ping_pong@example.org" ]
+}
diff --git a/native-messaging/app/ping_pong.py b/native-messaging/app/ping_pong.py
new file mode 100755
index 0000000..c987f60
--- /dev/null
+++ b/native-messaging/app/ping_pong.py
@@ -0,0 +1,62 @@
+#!/usr/bin/env python
+
+import sys
+import json
+import struct
+
+try:
+ # Python 3.x version
+ # Read a message from stdin and decode it.
+ def getMessage():
+ rawLength = sys.stdin.buffer.read(4)
+ if len(rawLength) == 0:
+ sys.exit(0)
+ messageLength = struct.unpack('@I', rawLength)[0]
+ message = sys.stdin.buffer.read(messageLength).decode('utf-8')
+ return json.loads(message)
+
+ # Encode a message for transmission,
+ # given its content.
+ def encodeMessage(messageContent):
+ encodedContent = json.dumps(messageContent).encode('utf-8')
+ encodedLength = struct.pack('@I', len(encodedContent))
+ return {'length': encodedLength, 'content': encodedContent}
+
+ # Send an encoded message to stdout
+ def sendMessage(encodedMessage):
+ sys.stdout.buffer.write(encodedMessage['length'])
+ sys.stdout.buffer.write(encodedMessage['content'])
+ sys.stdout.buffer.flush()
+
+ while True:
+ receivedMessage = getMessage()
+ if receivedMessage == "ping":
+ sendMessage(encodeMessage("pong3"))
+except AttributeError:
+ # Python 2.x version (if sys.stdin.buffer is not defined)
+ # Read a message from stdin and decode it.
+ def getMessage():
+ rawLength = sys.stdin.read(4)
+ if len(rawLength) == 0:
+ sys.exit(0)
+ messageLength = struct.unpack('@I', rawLength)[0]
+ message = sys.stdin.read(messageLength)
+ return json.loads(message)
+
+ # Encode a message for transmission,
+ # given its content.
+ def encodeMessage(messageContent):
+ encodedContent = json.dumps(messageContent)
+ encodedLength = struct.pack('@I', len(encodedContent))
+ return {'length': encodedLength, 'content': encodedContent}
+
+ # Send an encoded message to stdout
+ def sendMessage(encodedMessage):
+ sys.stdout.write(encodedMessage['length'])
+ sys.stdout.write(encodedMessage['content'])
+ sys.stdout.flush()
+
+ while True:
+ receivedMessage = getMessage()
+ if receivedMessage == "ping":
+ sendMessage(encodeMessage("pong2"))
diff --git a/native-messaging/app/ping_pong_win.bat b/native-messaging/app/ping_pong_win.bat
new file mode 100644
index 0000000..aac2019
--- /dev/null
+++ b/native-messaging/app/ping_pong_win.bat
@@ -0,0 +1,3 @@
+@echo off
+
+call python C:\path\to\ping_pong.py
diff --git a/navigation-stats/README.md b/navigation-stats/README.md
new file mode 100644
index 0000000..d30d5aa
--- /dev/null
+++ b/navigation-stats/README.md
@@ -0,0 +1,27 @@
+# navigation-stats
+
+## What it does ##
+
+The extension includes:
+
+* a background which collects navigation stats using the webNavigation API,
+ and store the stats using the storage API.
+* a browser action with a popup including HTML, CSS, and JS, which renders
+ the stats stored by the background page
+
+
+When the user navigate on a website from any of the browser tabs, the background
+page collected every completed navigation with the "http" or "https" schemes
+(using an UrlFilter for the listener of the webNavigation events)
+
+When the user clicks the browser action button, the popup is shown, and
+the stats saved using the storage API are retrived and rendered in the
+popup window.
+
+## What it shows ##
+
+* use the webNavigation API to monitor browsing navigation events
+* use an UrlFilter to only receive the webNavigation event using
+ one of the supported criteria.
+* use the storage API to persist data over browser reboots and to share it
+ between different extension pages.
diff --git a/navigation-stats/background.js b/navigation-stats/background.js
new file mode 100644
index 0000000..0dee190
--- /dev/null
+++ b/navigation-stats/background.js
@@ -0,0 +1,30 @@
+// Load existent stats with the storage API.
+var gettingStoredStats = browser.storage.local.get("hostNavigationStats");
+gettingStoredStats.then(results => {
+ // Initialize the saved stats if not yet initialized.
+ if (!results.hostNavigationStats) {
+ results = {
+ hostNavigationStats: {}
+ };
+ }
+
+ const {hostNavigationStats} = results;
+
+ // Monitor completed navigation events and update
+ // stats accordingly.
+ browser.webNavigation.onCompleted.addListener(evt => {
+ // Filter out any sub-frame related navigation event
+ if (evt.frameId !== 0) {
+ return;
+ }
+
+ const url = new URL(evt.url);
+
+ hostNavigationStats[url.hostname] = hostNavigationStats[url.hostname] || 0;
+ hostNavigationStats[url.hostname]++;
+
+ // Persist the updated stats.
+ browser.storage.local.set(results);
+ }, {
+ url: [{schemes: ["http", "https"]}]});
+});
diff --git a/navigation-stats/icons/LICENSE b/navigation-stats/icons/LICENSE
new file mode 100644
index 0000000..e878a43
--- /dev/null
+++ b/navigation-stats/icons/LICENSE
@@ -0,0 +1 @@
+The icon "icon-32.png" is taken from the IconBeast Lite iconset, and used under the terms of its license (http://www.iconbeast.com/faq/), with a link back to the website: http://www.iconbeast.com/free/.
diff --git a/navigation-stats/icons/icon-32.png b/navigation-stats/icons/icon-32.png
new file mode 100644
index 0000000..b77538f
Binary files /dev/null and b/navigation-stats/icons/icon-32.png differ
diff --git a/navigation-stats/manifest.json b/navigation-stats/manifest.json
new file mode 100644
index 0000000..fac27f5
--- /dev/null
+++ b/navigation-stats/manifest.json
@@ -0,0 +1,24 @@
+{
+ "manifest_version": 2,
+ "name": "Navigation Stats",
+ "version": "0.1.0",
+ "browser_action": {
+ "default_icon": {
+ "32": "icons/icon-32.png"
+ },
+ "default_title": "Navigation Stats",
+ "default_popup": "popup.html"
+ },
+ "permissions": ["webNavigation", "storage"],
+ "background": {
+ "scripts": [ "background.js" ]
+ },
+ "icons": {
+ "32": "icons/icon-32.png"
+ },
+ "applications": {
+ "gecko": {
+ "strict_min_version": "50.0"
+ }
+ }
+}
diff --git a/navigation-stats/popup.html b/navigation-stats/popup.html
new file mode 100644
index 0000000..2788d4c
--- /dev/null
+++ b/navigation-stats/popup.html
@@ -0,0 +1,10 @@
+
+
+
+
The history permission is an optional permission that can be
+ granted or
+ revoked.
+
+
The cookies permission cannot be granted, because it was not included in the optional_permissions manifest key.
+
The foo permission cannot be granted, because it is not a valid permission name.
+
+
+
+
+
diff --git a/permissions/page.js b/permissions/page.js
new file mode 100644
index 0000000..25b9770
--- /dev/null
+++ b/permissions/page.js
@@ -0,0 +1,46 @@
+function updatePermissions() {
+ browser.permissions.getAll()
+ .then((permissions) => {
+ document.getElementById('permissions').innerText = permissions.permissions.join(', ');
+ });
+}
+
+async function processChange(event) {
+ let permission = event.target.dataset.permission;
+ let result = document.getElementById('result');
+
+ try {
+ if (event.target.dataset.action === 'grant') {
+ browser.permissions.request({permissions: [permission]})
+ .then((response) => {
+ result.className = 'bg-success';
+ result.textContent = 'Call successful.';
+ })
+ .catch((err) => {
+ // Catch the case where the permission cannot be granted.
+ result.className = 'bg-warning';
+ result.textContent = err.message;
+ });
+ }
+ else {
+ browser.permissions.remove({permissions: [permission]})
+ .then((response) => {
+ result.className = 'bg-success';
+ result.textContent = 'Call successful.';
+ });
+ }
+ } catch(err) {
+ // Catch the case where the permission is completely wrong.
+ result.className = 'bg-danger';
+ result.textContent = err.message;
+ }
+ result.style.display = 'block';
+ updatePermissions();
+ event.preventDefault();
+}
+
+for (let element of document.getElementsByClassName('permission')) {
+ element.addEventListener('click', processChange);
+}
+
+updatePermissions();
diff --git a/proxy-blocker/README.md b/proxy-blocker/README.md
new file mode 100644
index 0000000..926db7f
--- /dev/null
+++ b/proxy-blocker/README.md
@@ -0,0 +1,22 @@
+# proxy-filter
+
+## What it does
+
+This add-on registers a [PAC script](https://developer.mozilla.org/en-US/docs/Web/HTTP/Proxy_servers_and_tunneling/Proxy_Auto-Configuration_%28PAC%29_file) using the proxy API.
+
+The PAC script is initialized with a list of hostnames: it blocks requests to any hosts that are in the list.
+
+The list is given the following default values: `["example.com", "example.org"]`, but the user can add and remove hosts using the add-on's options page.
+
+Note that the hostname-matching is very simple: hostnames must exactly match an entry in the list if they are to be blocked. So with the default settings, "example.org" would be blocked but "www.example.org" would be permitted.
+
+To try it out:
+* install it
+* try visiting `http://example.com`, and see it is blocked
+* visit `about:addons`, open the add-on's preferences, and try changing the hostnames in the text box
+* try visiting some different pages, to see the effect of your changes.
+
+## What it shows
+
+* How to implement a simple PAC script, and register it using the proxy API.
+* How to exchange messages between a PAC script and a background script.
diff --git a/proxy-blocker/background/proxy-handler.js b/proxy-blocker/background/proxy-handler.js
new file mode 100644
index 0000000..4a0ff0b
--- /dev/null
+++ b/proxy-blocker/background/proxy-handler.js
@@ -0,0 +1,55 @@
+// Location of the proxy script, relative to manifest.json
+const proxyScriptURL = "proxy/proxy-script.js";
+
+// Default settings. If there is nothing in storage, use these values.
+const defaultSettings = {
+ blockedHosts: ["example.com", "example.org"]
+ }
+
+// Register the proxy script
+browser.proxy.register(proxyScriptURL);
+
+// Log any errors from the proxy script
+browser.proxy.onProxyError.addListener(error => {
+ console.error(`Proxy error: ${error.message}`);
+});
+
+// Initialize the proxy
+function handleInit() {
+ // update the proxy whenever stored settings change
+ browser.storage.onChanged.addListener((newSettings) => {
+ browser.runtime.sendMessage(newSettings.blockedHosts.newValue, {toProxyScript: true});
+ });
+
+ // get the current settings, then...
+ browser.storage.local.get()
+ .then((storedSettings) => {
+ // if there are stored settings, update the proxy with them...
+ if (storedSettings.blockedHosts) {
+ browser.runtime.sendMessage(storedSettings.blockedHosts, {toProxyScript: true});
+ // ...otherwise, initialize storage with the default values
+ } else {
+ browser.storage.local.set(defaultSettings);
+ }
+
+ })
+ .catch(()=> {
+ console.log("Error retrieving stored settings");
+ });
+}
+
+function handleMessage(message, sender) {
+ // only handle messages from the proxy script
+ if (sender.url != browser.extension.getURL(proxyScriptURL)) {
+ return;
+ }
+
+ if (message === "init") {
+ handleInit(message);
+ } else {
+ // after the init message the only other messages are status messages
+ console.log(message);
+ }
+}
+
+browser.runtime.onMessage.addListener(handleMessage);
diff --git a/proxy-blocker/icons/LICENSE b/proxy-blocker/icons/LICENSE
new file mode 100644
index 0000000..f39164e
--- /dev/null
+++ b/proxy-blocker/icons/LICENSE
@@ -0,0 +1 @@
+The "lock".svg" icon is taken from the Material Core iconset and is used under the terms of its license: https://www.iconfinder.com/iconsets/material-core.
diff --git a/proxy-blocker/icons/block.svg b/proxy-blocker/icons/block.svg
new file mode 100644
index 0000000..da360e2
--- /dev/null
+++ b/proxy-blocker/icons/block.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/proxy-blocker/manifest.json b/proxy-blocker/manifest.json
new file mode 100644
index 0000000..da36852
--- /dev/null
+++ b/proxy-blocker/manifest.json
@@ -0,0 +1,32 @@
+{
+
+ "manifest_version": 2,
+ "name": "Proxy-blocker",
+ "description": "Uses the proxy API to block requests to specific hosts.",
+ "version": "1.0",
+
+ "icons": {
+ "48": "icons/block.svg",
+ "96": "icons/block.svg"
+ },
+
+ "applications": {
+ "gecko": {
+ "strict_min_version": "56.0a1"
+ }
+ },
+
+ "background": {
+ "scripts": [
+ "background/proxy-handler.js"
+ ]
+ },
+
+ "options_ui": {
+ "page": "options/options.html",
+ "browser_style": true
+ },
+
+ "permissions": ["proxy", "storage"]
+
+}
diff --git a/proxy-blocker/options/options.css b/proxy-blocker/options/options.css
new file mode 100644
index 0000000..ac9d052
--- /dev/null
+++ b/proxy-blocker/options/options.css
@@ -0,0 +1,4 @@
+#blocked-hosts {
+ display: block;
+ margin-top: 1em;
+}
diff --git a/proxy-blocker/options/options.html b/proxy-blocker/options/options.html
new file mode 100644
index 0000000..7cbcf60
--- /dev/null
+++ b/proxy-blocker/options/options.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+ Hosts to block:
+
+
+
+
+
+
+
diff --git a/proxy-blocker/options/options.js b/proxy-blocker/options/options.js
new file mode 100644
index 0000000..78af2d1
--- /dev/null
+++ b/proxy-blocker/options/options.js
@@ -0,0 +1,25 @@
+const blockedHostsTextArea = document.querySelector("#blocked-hosts");
+
+// Store the currently selected settings using browser.storage.local.
+function storeSettings() {
+ let blockedHosts = blockedHostsTextArea.value.split("\n");
+ browser.storage.local.set({
+ blockedHosts
+ });
+}
+
+// Update the options UI with the settings values retrieved from storage,
+// or the default settings if the stored settings are empty.
+function updateUI(restoredSettings) {
+ blockedHostsTextArea.value = restoredSettings.blockedHosts.join("\n");
+}
+
+function onError(e) {
+ console.error(e);
+}
+
+// On opening the options page, fetch stored settings and update the UI with them.
+browser.storage.local.get().then(updateUI, onError);
+
+// Whenever the contents of the textarea changes, save the new values
+blockedHostsTextArea.addEventListener("change", storeSettings);
diff --git a/proxy-blocker/proxy/proxy-script.js b/proxy-blocker/proxy/proxy-script.js
new file mode 100644
index 0000000..4a133fb
--- /dev/null
+++ b/proxy-blocker/proxy/proxy-script.js
@@ -0,0 +1,23 @@
+/* exported FindProxyForURL */
+
+var blockedHosts = [];
+const allow = "DIRECT";
+const deny = "PROXY 127.0.0.1:65535";
+
+// tell the background script that we are ready
+browser.runtime.sendMessage("init");
+
+// listen for updates to the blocked host list
+browser.runtime.onMessage.addListener((message) => {
+ blockedHosts = message;
+});
+
+// required PAC function that will be called to determine
+// if a proxy should be used.
+function FindProxyForURL(url, host) {
+ if (blockedHosts.indexOf(host) != -1) {
+ browser.runtime.sendMessage(`Proxy-blocker: blocked ${url}`);
+ return deny;
+ }
+ return allow;
+}
diff --git a/quicknote/README.md b/quicknote/README.md
index d679321..3d66bba 100644
--- a/quicknote/README.md
+++ b/quicknote/README.md
@@ -1,7 +1,14 @@
# Quicknote
+
A persistent note/to-do list application — click a button in your browser and record notes, which will persist even after browser restarts.
-Works in Firefox 47+, and will also work as a Chrome extension, out of the box.
+Works in Firefox 47+.
+
+## Running with web-ext
+
+[web-ext](https://developer.mozilla.org/en-US/Add-ons/WebExtensions) generates a new profile on each run, meaning your data is not persisted between Firefox runs. To use web-ext and preserve this information, you will need an existing or new Firefox profile. Then run:
+
+web-ext run --firefox-profile [A PATH TO A FIREFOX PROFILE] --keep-profile-changes
## What it does
@@ -9,12 +16,12 @@ This extension includes:
* A browser action that creates a popup — within the popup is:
* Two form elements for entering title and body text for a new note, along with a button to add a note, and a button to clear all notes.
- * A list of the notes that have been added to the extension — each note includes a delete button to delete just that extension. You can also click on the note title and body to edit them. In edit mode, each note includes:
+ * A list of the notes that have been added to the extension — each note includes a delete button to delete just that note. You can also click on the note title and body to edit them. In edit mode, each note includes:
* An update button to submit an update.
* A cancel button to cancel the update.
-
+
Quicknote uses the WebExtensions [Storage API](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/storage) to persist the notes.
## What it shows
-* How to persist data in a WebExtension using the Storage API.
\ No newline at end of file
+* How to persist data in a WebExtension using the Storage API.
diff --git a/quicknote/manifest.json b/quicknote/manifest.json
index 599ec05..ba5f2e5 100644
--- a/quicknote/manifest.json
+++ b/quicknote/manifest.json
@@ -2,23 +2,16 @@
"manifest_version": 2,
"name": "Quicknote",
- "version": "1.0",
+ "version": "1.1",
"description": "Allows the user to make quick notes by clicking a button and entering text into the resulting popup. The notes are saved in storage. See https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Examples#quicknote",
"icons": {
"48": "icons/quicknote-48.png"
},
- "applications": {
- "gecko": {
- "id": "quicknote@mozilla.org",
- "strict_min_version": "45.0"
- }
- },
-
"permissions": [
"storage"
- ],
+ ],
"browser_action": {
"default_icon": {
@@ -26,5 +19,12 @@
},
"default_title": "Quicknote",
"default_popup": "popup/quicknote.html"
+ },
+
+ "applications": {
+ "gecko": {
+ "id": "quicknote-example@mozilla.org"
+ }
}
+
}
diff --git a/quicknote/popup/quicknote.js b/quicknote/popup/quicknote.js
index a765b26..04d5a81 100644
--- a/quicknote/popup/quicknote.js
+++ b/quicknote/popup/quicknote.js
@@ -14,23 +14,24 @@ var addBtn = document.querySelector('.add');
addBtn.addEventListener('click', addNote);
clearBtn.addEventListener('click', clearAll);
+/* generic error handler */
+function onError(error) {
+ console.log(error);
+}
+
/* display previously-saved stored notes on startup */
initialize();
function initialize() {
- chrome.storage.local.get(null,function(results) {
- if(chrome.runtime.lastError) {
- console.log(chrome.runtime.lastError);
- } else {
- var noteKeys = Object.keys(results);
- for(i = 0; i < noteKeys.length; i++) {
- var curKey = noteKeys[i];
- var curValue = results[curKey];
- displayNote(curKey,curValue);
- }
+ var gettingAllStorageItems = browser.storage.local.get(null);
+ gettingAllStorageItems.then((results) => {
+ var noteKeys = Object.keys(results);
+ for (let noteKey of noteKeys) {
+ var curValue = results[noteKey];
+ displayNote(noteKey,curValue);
}
- });
+ }, onError);
}
/* Add a note to the display, and storage */
@@ -38,26 +39,24 @@ function initialize() {
function addNote() {
var noteTitle = inputTitle.value;
var noteBody = inputBody.value;
- chrome.storage.local.get(noteTitle, function(result) {
+ var gettingItem = browser.storage.local.get(noteTitle);
+ gettingItem.then((result) => {
var objTest = Object.keys(result);
if(objTest.length < 1 && noteTitle !== '' && noteBody !== '') {
inputTitle.value = '';
inputBody.value = '';
storeNote(noteTitle,noteBody);
}
- })
+ }, onError);
}
/* function to store a new note in storage */
function storeNote(title, body) {
- chrome.storage.local.set({ [title] : body }, function() {
- if(chrome.runtime.lastError) {
- console.log(chrome.runtime.lastError);
- } else {
- displayNote(title,body);
- }
- });
+ var storingNote = browser.storage.local.set({ [title] : body });
+ storingNote.then(() => {
+ displayNote(title,body);
+ }, onError);
}
/* function to display a note in the note box */
@@ -89,10 +88,10 @@ 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);
- chrome.storage.local.remove(title);
+ browser.storage.local.remove(title);
})
/* create note edit box */
@@ -126,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);
@@ -155,15 +154,17 @@ function displayNote(title, body) {
/* function to update notes */
function updateNote(delNote,newTitle,newBody) {
- chrome.storage.local.set({ [newTitle] : newBody }, function() {
+ var storingNote = browser.storage.local.set({ [newTitle] : newBody });
+ storingNote.then(() => {
if(delNote !== newTitle) {
- chrome.storage.local.remove(delNote, function() {
+ var removingNote = browser.storage.local.remove(delNote);
+ removingNote.then(() => {
displayNote(newTitle, newBody);
- });
+ }, onError);
} else {
displayNote(newTitle, newBody);
}
- });
+ }, onError);
}
/* Clear all notes from the display/storage */
@@ -172,5 +173,5 @@ function clearAll() {
while (noteContainer.firstChild) {
noteContainer.removeChild(noteContainer.firstChild);
}
- chrome.storage.local.clear();
-}
\ No newline at end of file
+ browser.storage.local.clear();
+}
diff --git a/react-es6-popup/.babelrc b/react-es6-popup/.babelrc
new file mode 100644
index 0000000..ffd1d11
--- /dev/null
+++ b/react-es6-popup/.babelrc
@@ -0,0 +1,11 @@
+{
+ "presets": [
+ "es2015",
+ "stage-2",
+ "react"
+ ],
+ "plugins": [
+ "transform-class-properties",
+ "transform-es2015-modules-commonjs"
+ ]
+}
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/.gitignore b/react-es6-popup/.gitignore
new file mode 100644
index 0000000..20ccaa9
--- /dev/null
+++ b/react-es6-popup/.gitignore
@@ -0,0 +1,5 @@
+# Ignore build artifacts and other files.
+.DS_Store
+yarn.lock
+extension/dist
+node_modules
diff --git a/react-es6-popup/.npmrc b/react-es6-popup/.npmrc
new file mode 100644
index 0000000..a00908d
--- /dev/null
+++ b/react-es6-popup/.npmrc
@@ -0,0 +1 @@
+save-prefix=''
diff --git a/react-es6-popup/README.md b/react-es6-popup/README.md
new file mode 100644
index 0000000..5fa69cf
--- /dev/null
+++ b/react-es6-popup/README.md
@@ -0,0 +1,54 @@
+# React / ES6 Popup Example
+
+## What it does
+
+This is an example of creating a browser action
+[popup](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Add_a_button_to_the_toolbar#Adding_a_popup)
+UI in [React][react] and [ES6](http://es6-features.org/) JavaScript.
+
+## What it shows
+
+* How to bundle [React][react] and any other [NodeJS][nodejs] module into an
+ extension.
+* How to transpile code that is not supported natively in
+ a browser such as
+ [import / export](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import)
+ syntax and [JSX](https://facebook.github.io/react/docs/jsx-in-depth.html).
+* How to continuously build code as you edit files.
+* How to customize [web-ext][web-ext] for your extension's specific needs.
+* How to structure your code in reusable ES6 modules.
+
+## Usage
+
+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
+
+Start the continuous build process to transpile the code into something that
+can run in Firefox or Chrome:
+
+ npm run build
+
+This creates a WebExtension in the `extension` subdirectory.
+Any time you edit a file, it will be rebuilt automatically.
+
+In another shell window, run the extension in Firefox using a wrapper
+around [web-ext][web-ext]:
+
+ npm start
+
+Any time you edit a file, [web-ext][web-ext] will reload the extension
+in Firefox. To see the popup, click the watermelon icon from the browser bar.
+Here is what it looks like:
+
+
+
+[react]: https://facebook.github.io/react/
+[nodejs]: https://nodejs.org/en/
+[web-ext]: https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Getting_started_with_web-ext
+
+## Icons
+
+The icon for this extension is provided by [icons8](https://icons8.com/).
diff --git a/react-es6-popup/extension/images/Watermelon-48.png b/react-es6-popup/extension/images/Watermelon-48.png
new file mode 100644
index 0000000..2821b3f
Binary files /dev/null and b/react-es6-popup/extension/images/Watermelon-48.png differ
diff --git a/react-es6-popup/extension/images/Watermelon-96.png b/react-es6-popup/extension/images/Watermelon-96.png
new file mode 100644
index 0000000..0b4f4f1
Binary files /dev/null and b/react-es6-popup/extension/images/Watermelon-96.png differ
diff --git a/react-es6-popup/extension/manifest.json b/react-es6-popup/extension/manifest.json
new file mode 100755
index 0000000..5126bdd
--- /dev/null
+++ b/react-es6-popup/extension/manifest.json
@@ -0,0 +1,17 @@
+{
+ "manifest_version": 2,
+ "name": "react-es6-popup-example",
+ "version": "1.0",
+
+ "browser_action": {
+ "browser_style": true,
+ "default_icon": {
+ "48": "images/Watermelon-48.png",
+ "96": "images/Watermelon-96.png"
+ },
+ "default_title": "React Example",
+ "default_popup": "popup.html"
+ },
+
+ "permissions": ["activeTab"]
+}
diff --git a/react-es6-popup/extension/popup.css b/react-es6-popup/extension/popup.css
new file mode 100755
index 0000000..1b048c9
--- /dev/null
+++ b/react-es6-popup/extension/popup.css
@@ -0,0 +1,8 @@
+body {
+ width: 400px;
+ padding: 1em;
+}
+
+h1, h2 {
+ border-bottom: 1px solid;
+}
diff --git a/react-es6-popup/extension/popup.html b/react-es6-popup/extension/popup.html
new file mode 100755
index 0000000..a005aa0
--- /dev/null
+++ b/react-es6-popup/extension/popup.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/react-es6-popup/package.json b/react-es6-popup/package.json
new file mode 100644
index 0000000..e4930ae
--- /dev/null
+++ b/react-es6-popup/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "react-es6-popup-example",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "build": "webpack -w -v --display-error-details --progress --colors",
+ "start": "web-ext run -s extension/"
+ },
+ "author": "",
+ "license": "MPL-2.0",
+ "devDependencies": {
+ "babel-core": "6.20.0",
+ "babel-loader": "6.2.9",
+ "babel-plugin-transform-class-properties": "6.19.0",
+ "babel-plugin-transform-object-rest-spread": "6.20.2",
+ "babel-preset-es2015": "6.18.0",
+ "babel-preset-react": "6.16.0",
+ "babel-preset-stage-2": "6.18.0",
+ "react": "15.4.1",
+ "react-dom": "15.4.1",
+ "web-ext": "1.6.0",
+ "webpack": "1.14.0"
+ }
+}
diff --git a/react-es6-popup/screenshots/popup.png b/react-es6-popup/screenshots/popup.png
new file mode 100644
index 0000000..933d435
Binary files /dev/null and b/react-es6-popup/screenshots/popup.png differ
diff --git a/react-es6-popup/src/nested-component.js b/react-es6-popup/src/nested-component.js
new file mode 100644
index 0000000..1ca1496
--- /dev/null
+++ b/react-es6-popup/src/nested-component.js
@@ -0,0 +1,15 @@
+import React from 'react';
+
+export default class Nested extends React.Component {
+ render() {
+ return (
+
+
Nested Component
+
+ This is an example of a nested component that was imported via
+ import / export syntax.
+
+
+ );
+ }
+}
diff --git a/react-es6-popup/src/popup.js b/react-es6-popup/src/popup.js
new file mode 100755
index 0000000..52da0ca
--- /dev/null
+++ b/react-es6-popup/src/popup.js
@@ -0,0 +1,36 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+
+import Nested from './nested-component';
+
+class Popup extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {activeTab: null};
+ }
+
+ componentDidMount() {
+ // Get the active tab and store it in component state.
+ browser.tabs.query({active: true}).then(tabs => {
+ this.setState({activeTab: tabs[0]});
+ });
+ }
+
+ render() {
+ const {activeTab} = this.state;
+ return (
+
+
React Component
+
+ This is an example of a popup UI in React.
+
+
+ Active tab: {activeTab ? activeTab.url : '[waiting for result]'}
+
+
+
+ );
+ }
+}
+
+ReactDOM.render(, document.getElementById('app'));
diff --git a/react-es6-popup/webpack.config.js b/react-es6-popup/webpack.config.js
new file mode 100644
index 0000000..c6decf7
--- /dev/null
+++ b/react-es6-popup/webpack.config.js
@@ -0,0 +1,48 @@
+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: './src/background.js',
+ popup: './src/popup.js',
+ },
+ output: {
+ // This copies each source entry into the extension dist folder named
+ // after its entry config key.
+ path: 'extension/dist',
+ filename: '[name].js',
+ },
+ module: {
+ // This transpiles all code (except for third party modules) using Babel.
+ loaders: [{
+ exclude: /node_modules/,
+ test: /\.js$/,
+ // Babel options are in .babelrc
+ loaders: ['babel'],
+ }],
+ },
+ resolve: {
+ // This allows you to import modules just like you would in a NodeJS app.
+ extensions: ['', '.js', '.jsx'],
+ root: [
+ path.resolve(__dirname),
+ ],
+ modulesDirectories: [
+ '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/selection-to-clipboard/README.md b/selection-to-clipboard/README.md
new file mode 100644
index 0000000..5169b7b
--- /dev/null
+++ b/selection-to-clipboard/README.md
@@ -0,0 +1,23 @@
+# selection-to-clipboard
+
+**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
+
+This extension includes:
+
+* a content script, "content-script.js", that is injected into all pages
+
+The content script listens for text selections in the page it's attached to and copies the text to the clipboard on mouse-up.
+
+## What it shows
+
+* how to inject content scripts declaratively using manifest.json
+* how to write to the [clipboard](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Interact_with_the_clipboard)
+
+## Note
+* If the `copySelection` function was in a browser event `clipboardWrite` permissions would be required e.g.
+```
+"permissions": ["clipboardWrite"]
+```
+See [Interact with the clipboard](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Interact_with_the_clipboard.)
diff --git a/selection-to-clipboard/content-script.js b/selection-to-clipboard/content-script.js
new file mode 100644
index 0000000..1b78c5b
--- /dev/null
+++ b/selection-to-clipboard/content-script.js
@@ -0,0 +1,15 @@
+/*
+copy the selected text to clipboard
+*/
+function copySelection() {
+ var selectedText = window.getSelection().toString().trim();
+
+ if (selectedText) {
+ document.execCommand("Copy");
+ }
+}
+
+/*
+Add copySelection() as a listener to mouseup events.
+*/
+document.addEventListener("mouseup", copySelection);
\ No newline at end of file
diff --git a/selection-to-clipboard/icons/LICENSE b/selection-to-clipboard/icons/LICENSE
new file mode 100644
index 0000000..20e821d
--- /dev/null
+++ b/selection-to-clipboard/icons/LICENSE
@@ -0,0 +1,2 @@
+
+The "page-32.png" and "page-48.png" icons are taken from the miu iconset created by Linh Pham Thi Dieu, and are used under the terms of its license: http://linhpham.me/miu/.
diff --git a/selection-to-clipboard/icons/clipboard-48.png b/selection-to-clipboard/icons/clipboard-48.png
new file mode 100644
index 0000000..387f55f
Binary files /dev/null and b/selection-to-clipboard/icons/clipboard-48.png differ
diff --git a/selection-to-clipboard/manifest.json b/selection-to-clipboard/manifest.json
new file mode 100644
index 0000000..39f8b35
--- /dev/null
+++ b/selection-to-clipboard/manifest.json
@@ -0,0 +1,16 @@
+{
+ "manifest_version": 2,
+ "name": "selection-to-clipboard",
+ "description": "Example of WebExtensionAPI for writing to the clipboard",
+ "version": "1.0",
+ "homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/selection-to-clipboard",
+
+ "icons": {
+ "48": "icons/clipboard-48.png"
+ },
+
+ "content_scripts": [{
+ "matches": [""],
+ "js": ["content-script.js"]
+ }]
+}
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.
+
+[](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":";;;;;;;;;;;;;AAAA;;AAEA;;;;;;;;AAQA;;;;;;;;;;;;AAYA;;;;;;;;;;;;;;;;;UAagBA,iB,GAAAA,iB;UAqtBMC,c,GAAAA,c;AArtBf,WAASD,iBAAT,CAA2BE,GAA3B,EAAgCC,SAAhC,EAA2C;AAChD,WAAO,IAAIC,OAAJ,CAAY,CAACC,OAAD,EAAUC,MAAV,KAAqB;AACtCJ,UAAIC,SAAJ,GAAgBA,YACb,MAAME,QAAQF,UAAUD,IAAIK,MAAd,CAAR,CADO,GAC4B,MAAMF,QAAQH,IAAIK,MAAZ,CADlD;AAEAL,UAAIM,OAAJ,GAAc,MAAMF,OAAOJ,IAAIO,KAAX,CAApB;AACD,KAJM,CAAP;AAKD;;AAED;;;;;;AAMO,QAAMC,qBAAN,CAA4B;AACjC;;;AAGAC,gBAAY,EAACC,IAAD,EAAOC,UAAP,EAAZ,EAAgC;AAC9B;AACA;;AAEA;AACA,WAAKD,IAAL,GAAYA,IAAZ;AACA;AACA,WAAKC,UAAL,GAAkBA,UAAlB;AACA;AACA,WAAKC,UAAL,GAAkBV,QAAQC,OAAR,EAAlB;AACA;AACA,WAAKU,MAAL,GAAcC,SAAd;AACA;AACA,WAAKC,OAAL,GAAeD,SAAf;AACD;;AAED;;;AAGAE,iBAAa,EAACC,WAAD,KAAgB,EAA7B,EAAiC;AAC/B,UAAI,KAAKJ,MAAT,EAAiB;AACf,cAAM,IAAIK,KAAJ,CAAU,4BAAV,CAAN;AACD;;AAED,UAAI,KAAKH,OAAT,EAAkB;AAChB,cAAM,IAAIG,KAAJ,CAAU,6BAAV,CAAN;AACD;;AAED,UAAI,CAAC,KAAKP,UAAV,EAAsB;AACpB,cAAM,IAAIO,KAAJ,CAAU,qBAAV,CAAN;AACD;;AAED,UAAID,eAAe,KAAKN,UAAL,CAAgBQ,IAAhB,KAAyBF,WAA5C,EAAyD;AACvD,cAAM,IAAIC,KAAJ,CAAW,uCAAsC,KAAKP,UAAL,CAAgBQ,IAAK,GAAtE,CAAN;AACD;AACD,UAAI,CAAC,KAAKR,UAAL,CAAgBS,MAArB,EAA6B;AAC3B;AACA,aAAKV,IAAL,CAAUW,gBAAV,CAA2B,IAA3B;AACD;AACF;;AAED;;AAEA;;;;;AAKA,QAAIF,IAAJ,GAAW;AACT,aAAO,KAAKR,UAAL,CAAgBQ,IAAvB;AACD;;AAED;;;;;AAKA,QAAIC,MAAJ,GAAa;AACX,aAAO,KAAKT,UAAL,GAAkB,KAAKA,UAAL,CAAgBS,MAAlC,GAA2C,KAAlD;AACD;;AAED;;;;;;AAMA,UAAME,KAAN,GAAc;AACZ,UAAI,CAAC,KAAKX,UAAV,EAAsB;AACpB,cAAM,IAAIO,KAAJ,CAAU,wBAAV,CAAN;AACD;;AAED;AACA,YAAM,KAAKN,UAAX;;AAEA;AACA,UAAI,KAAKD,UAAL,CAAgBS,MAAhB,IAA0B,KAAKT,UAAL,CAAgBQ,IAAhB,KAAyB,UAAvD,EAAmE;AACjE,cAAMrB,kBAAkB,KAAKa,UAAL,CAAgBY,KAAhB,EAAlB,CAAN;AACD;;AAED,WAAKV,MAAL,GAAc,IAAd;AACA,WAAKF,UAAL,GAAkB,IAAlB;AACA,WAAKC,UAAL,GAAkBV,QAAQC,OAAR,EAAlB;AACD;;AAED;;;;;;AAMA,UAAMqB,KAAN,GAAc;AACZ,UAAI,KAAKb,UAAL,CAAgBS,MAApB,EAA4B;AAC1B;AACA;AACA,aAAKT,UAAL,CAAgBa,KAAhB;AACD;;AAED,WAAKT,OAAL,GAAe,IAAf;AACA,WAAKJ,UAAL,GAAkB,IAAlB;AACA,WAAKC,UAAL,GAAkBV,QAAQC,OAAR,EAAlB;AACD;;AAED;;;;;;AAMA,UAAMsB,WAAN,GAAoB;AAClB,WAAKT,YAAL;AACA,aAAOlB,kBAAkB,KAAKa,UAAL,CAAgBc,WAAhB,EAAlB,CAAP;AACD;;AAED;;;;;;;;;;;;AAYA,UAAMC,UAAN,CAAiBC,IAAjB,EAAuBC,QAAvB,EAAiC;AAC/B,WAAKZ,YAAL,CAAkB,EAACC,aAAa,WAAd,EAAlB;AACA,UAAI,OAAOW,QAAP,KAAoB,QAAxB,EAAkC;AAChC,aAAKjB,UAAL,CAAgBiB,QAAhB,GAA2BA,QAA3B;AACD;AACD,aAAO9B,kBAAkB,KAAKa,UAAL,CAAgBe,UAAhB,CAA2BC,IAA3B,CAAlB,CAAP;AACD;;AAED;;;;;;;;;;;;AAYA,UAAME,iBAAN,CAAwBF,IAAxB,EAA8BC,QAA9B,EAAwC;AACtC,WAAKZ,YAAL,CAAkB,EAACC,aAAa,WAAd,EAAlB;AACA,UAAI,OAAOW,QAAP,KAAoB,QAAxB,EAAkC;AAChC,aAAKjB,UAAL,CAAgBiB,QAAhB,GAA2BA,QAA3B;AACD;AACD,aAAO9B,kBAAkB,KAAKa,UAAL,CAAgBkB,iBAAhB,CAAkCF,IAAlC,CAAlB,CAAP;AACD;;AAED;;;;;;;;;AASA,UAAMG,QAAN,CAAeF,WAAW,CAA1B,EAA6B;AAC3B,WAAKZ,YAAL,CAAkB,EAACC,aAAa,UAAd,EAAlB;AACA,aAAOnB,kBAAkB,KAAKa,UAAL,CAAgBmB,QAAhB,CAAyBF,QAAzB,CAAlB,CAAP;AACD;;AAED;;;;;;;;;AASA,UAAMG,MAAN,CAAaC,IAAb,EAAmB;AACjB,WAAKhB,YAAL,CAAkB,EAACC,aAAa,UAAd,EAAlB;AACA,aAAOnB,kBAAkB,KAAKa,UAAL,CAAgBoB,MAAhB,CAAuBC,IAAvB,CAAlB,CAAP;AACD;;AAED;;;;;;;;;;;AAWA,UAAMC,KAAN,CAAYD,IAAZ,EAAkBJ,QAAlB,EAA4B;AAC1B,WAAKZ,YAAL,CAAkB,EAACC,aAAa,UAAd,EAAlB;AACA,UAAI,OAAOW,QAAP,KAAoB,QAAxB,EAAkC;AAChC,aAAKjB,UAAL,CAAgBiB,QAAhB,GAA2BA,QAA3B;AACD;AACD,aAAO9B,kBACL,KAAKa,UAAL,CAAgBsB,KAAhB,CAAsBD,IAAtB,CADK;AAEL;AACA,YAAM;AACJ,eAAO,KAAKrB,UAAL,CAAgBiB,QAAvB;AACD,OALI,CAAP;AAOD;;AAED;;;;;;;;;;;;;AAaAM,gBAAYF,IAAZ,EAAkBJ,QAAlB,EAA4B;AAC1B,YAAMO,mBAAmB,MAAMC,YAAN,IAAsB;AAC7C,aAAKpB,YAAL,CAAkB,EAACC,aAAa,UAAd,EAAlB;;AAEA,YAAI,OAAOW,QAAP,KAAoB,QAAxB,EAAkC;AAChC,iBAAO,KAAKK,KAAL,CAAWD,IAAX,EAAiBJ,QAAjB,CAAP;AACD;AACD,eAAO,KAAKK,KAAL,CAAWD,IAAX,EAAiBI,YAAjB,CAAP;AACD,OAPD;;AASA,WAAKxB,UAAL,GAAkB,KAAKA,UAAL,CAAgByB,IAAhB,CAAqBF,gBAArB,CAAlB;AACA,aAAO,KAAKvB,UAAZ;AACD;;AAED;;;;;;;AAOA,UAAM0B,mBAAN,GAA4B;AAC1B,YAAM,KAAK1B,UAAX;AACD;AAvPgC;;UAAtBJ,qB,GAAAA,qB;AA0Pb;;;;;;AAMO,QAAM+B,sBAAN,CAA6B;AAClC;;;AAGA9B,gBAAY,EAAC+B,YAAD,EAAeC,GAAf,EAAoBC,QAApB,EAA8BC,QAA9B,EAAwCC,WAAxC,EAAZ,EAAkE;AAChE;AACA;;AAEA;AACA,WAAKJ,YAAL,GAAoBA,YAApB;AACA;AACA,WAAKC,GAAL,GAAWA,GAAX;AACA;AACA,WAAKC,QAAL,GAAgBA,QAAhB;AACA;AACA,WAAKC,QAAL,GAAgBA,QAAhB;AACA;AACA,WAAKC,WAAL,GAAmBA,WAAnB;AACD;;AAED;;;AAGAvB,qBAAiBwB,UAAjB,EAA6B;AAC3BA,iBAAWlC,UAAX,GAAwB,KAAKiC,WAAL,CAAiBE,IAAjB,CAAsBD,WAAW1B,IAAjC,CAAxB;AACD;;AAED;;AAEA;;;;;;;;;AASA2B,SAAK3B,IAAL,EAAW;AACT,UAAI,KAAKR,UAAT,EAAqB;AACnB,cAAM,IAAIO,KAAJ,CAAU,oCAAV,CAAN;AACD;AACD,YAAMP,aAAa,KAAKiC,WAAL,CAAiBE,IAAjB,CAAsB3B,IAAtB,CAAnB;;AAEA,aAAO,IAAIX,qBAAJ,CAA0B,EAACE,MAAM,IAAP,EAAaC,UAAb,EAA1B,CAAP;AACD;;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCAoC,cAAU;AACR,aAAOjD,kBAAkB,KAAK8C,WAAL,CAAiBG,OAAjB,EAAlB,CAAP;AACD;;AAED;;;;;;;;;;;;;;AAcA,UAAMC,qBAAN,CAA4BC,YAA5B,EAA0C;AACxC,UAAIA,iBAAiB,KAAKP,QAA1B,EAAoC;AAClC,cAAM,IAAIxB,KAAJ,CAAU,qDAAV,CAAN;AACD;;AAED,YAAMuB,MAAM,MAAM,KAAKD,YAAL,CAAkBU,aAAlB,EAAlB;AACA,YAAM,KAAKC,uBAAL,CAA6B,aAAa;AAC9C,cAAMzC,OAAO,MAAM,KAAKkC,WAAL,CAAiBG,OAAjB,EAAnB;AACA,cAAMK,cAAc,KAAKZ,YAAL,CAAkBa,yBAAlB,CAA4C,EAACZ,GAAD,EAAMtB,MAAM,WAAZ,EAA5C,CAApB;;AAEA,cAAMiC,YAAYE,GAAZ,CAAgB5C,IAAhB,EAAsBuC,YAAtB,CAAN;AACD,OALkC,CAKjCM,IALiC,CAK5B,IAL4B,CAA7B,CAAN;;AAOA,aAAO,KAAKf,YAAL,CAAkBgB,GAAlB,CAAsBP,YAAtB,CAAP;AACD;;AAED;;;;;;AAMAQ,cAAU;AACR,aAAO,KAAKjB,YAAL,CAAkBc,GAAlB,CAAsB,KAAKZ,QAA3B,EAAqC,IAArC,CAAP;AACD;;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,UAAMS,uBAAN,CAA8BO,iBAA9B,EAAiDvC,IAAjD,EAAuD;AACrD,UAAIuC,kBAAkBjD,WAAlB,CAA8BkD,IAA9B,KAAuC,mBAA3C,EAAgE;AAC9D,cAAM,IAAIzC,KAAJ,CAAU,uDAAV,CAAN;AACD;;AAED,YAAM,IAAIhB,OAAJ,CAAY,CAACC,OAAD,EAAUC,MAAV,KAAqB;AACrC,cAAMO,aAAa,KAAKiC,WAAL,CAAiBE,IAAjB,CAAsB3B,QAAQ,WAA9B,CAAnB;AACA,cAAMyC,mBAAmBF,kBAAkB/C,UAAlB,CAAzB;;AAEA,cAAMkD,yBAAyBC,qBAAqB;AAClD,gBAAMC,kBAAkBH,iBAAiBI,IAAjB,CAAsBF,iBAAtB,CAAxB;AACA,cAAIC,gBAAgBE,IAApB,EAA0B;AACxB9D;AACA;AACD,WAHD,MAGO,IAAI,EAAE4D,gBAAgBG,KAAhB,YAAiCC,OAAOC,UAAxC,IACAL,gBAAgBG,KAAhB,YAAiCC,OAAOE,UAD1C,CAAJ,EAC2D;AAChE,kBAAM9D,QAAQ,IAAIW,KAAJ,CAAU,6DAAV,CAAd;AACA0C,6BAAiBU,KAAjB,CAAuB/D,KAAvB;AACAH,mBAAOG,KAAP;AACA;AACD;;AAED,gBAAMgE,UAAUR,gBAAgBG,KAAhC;AACA,cAAIK,QAAQtE,SAAR,IAAqBsE,QAAQjE,OAAjC,EAA0C;AACxC,kBAAMC,QAAQ,IAAIW,KAAJ,CAAU,wDAAV,CAAd;AACA0C,6BAAiBU,KAAjB,CAAuB/D,KAAvB;AACAH,mBAAOG,KAAP;AACD,WAJD,MAIO;AACLgE,oBAAQtE,SAAR,GAAoB,MAAM4D,uBAAuBU,QAAQlE,MAA/B,CAA1B;AACAkE,oBAAQjE,OAAR,GAAkB,MAAMF,OAAOmE,QAAQhE,KAAf,CAAxB;AACD;AACF,SAtBD;;AAwBAsD;AACD,OA7BK,CAAN;AA8BD;AA9LiC;;UAAvBtB,sB,GAAAA,sB;AAiMb;;;;;;AAMO,QAAMiC,cAAN,CAAqB;AAC1B;;;AAGA/D,gBAAY,EAACkD,IAAD,EAAOc,UAAP,KAAqB,EAAjC,EAAqC;AACnC;AACA;;AAEA;AACA,WAAKd,IAAL,GAAYA,IAAZ;AACA;AACA,WAAKc,UAAL,GAAkBA,UAAlB;AACA;AACA,WAAKC,aAAL,GAAsB,sBAAqB,KAAKf,IAAK,EAArD;AACA;AACA,WAAKgB,iBAAL,GAAyB,uBAAzB;AACA;AACA,WAAKC,kBAAL,GAA0B9D,SAA1B;;AAEA;AACA;AACA,WAAK+D,OAAL,GAAe,GAAf;AACD;;AAED;;;AAGA3B,oBAAgB;AACd,UAAI,KAAK0B,kBAAT,EAA6B;AAC3B,eAAO,KAAKA,kBAAZ;AACD;;AAED,WAAKA,kBAAL,GAA0B,CAAC,YAAY;AACrC,YAAIT,OAAOW,cAAP,IAAyB,KAAKL,UAAlC,EAA8C;AAC5C,eAAKI,OAAL,GAAe,EAACA,SAAS,KAAKA,OAAf,EAAwBE,SAAS,YAAjC,EAAf;AACD;AACD,cAAMC,QAAQC,UAAUnC,IAAV,CAAe,KAAK4B,aAApB,EAAmC,KAAKG,OAAxC,CAAd;;AAEAG,cAAME,eAAN,GAAwB,MAAM;AAC5B,gBAAMC,KAAKH,MAAM3E,MAAjB;AACA,cAAI,CAAC8E,GAAGC,gBAAH,CAAoBC,QAApB,CAA6B,KAAKV,iBAAlC,CAAL,EAA2D;AACzDQ,eAAGG,iBAAH,CAAqB,KAAKX,iBAA1B;AACD;AACF,SALD;;AAOA,eAAO7E,kBAAkBkF,KAAlB,CAAP;AACD,OAdyB,GAA1B;;AAgBA,aAAO,KAAKJ,kBAAZ;AACD;;AAED;;;AAGAvB,8BAA0B,EAACZ,GAAD,EAAMtB,IAAN,KAAc,EAAxC,EAA4C;AAC1C,YAAMoE,cAAc9C,IAAI8C,WAAJ,CAAgB,CAAC,KAAKZ,iBAAN,CAAhB,EAA0CxD,IAA1C,CAApB;AACA,aAAOoE,YAAYnC,WAAZ,CAAwB,KAAKuB,iBAA7B,CAAP;AACD;;AAED;;;;;;;;;;;AAWA,UAAMa,iBAAN,CAAwB9C,QAAxB,EAAkCC,WAAW,MAA7C,EAAqD;AACnD,UAAI,CAACwB,OAAOW,cAAZ,EAA4B;AAC1B,cAAM,IAAI5D,KAAJ,CAAU,kDAAV,CAAN;AACD;AACD,YAAMuB,MAAM,MAAM,KAAKS,aAAL,EAAlB;AACA,YAAMN,cAAc,MAAM9C,kBACxB2C,IAAI+C,iBAAJ,CAAsB9C,QAAtB,EAAgCC,QAAhC,CADwB,CAA1B;AAGA,aAAO,IAAIJ,sBAAJ,CAA2B;AAChCC,sBAAc,IADkB,EACZC,GADY,EACPC,QADO,EACGC,QADH,EACaC;AADb,OAA3B,CAAP;AAGD;;AAED;;;;;;;;;;;;AAYA,UAAMU,GAAN,CAAUZ,QAAV,EAAoBhC,IAApB,EAA0B;AACxB,UAAI,CAACgC,QAAD,IAAa,OAAOA,QAAP,KAAoB,QAArC,EAA+C;AAC7C,cAAM,IAAIxB,KAAJ,CAAU,iCAAV,CAAN;AACD;;AAED,UAAI,EAAER,gBAAgB+E,IAAlB,KAA2B,EAAE/E,gBAAgBgF,IAAlB,CAA3B,IACA,EAAEvB,OAAOW,cAAP,IAAyBpE,gBAAgByD,OAAOW,cAAlD,CADA,IAEA,EAAEpE,gBAAgB6B,sBAAlB,CAFJ,EAE+C;AAC7C,cAAM,IAAIrB,KAAJ,CAAW,qBAAoBwB,QAAS,sBAAxC,CAAN;AACD;;AAED,UAAIhC,gBAAgB6B,sBAApB,EAA4C;AAC1C7B,eAAOA,KAAKkC,WAAZ;AACD;;AAED,YAAMH,MAAM,MAAM,KAAKS,aAAL,EAAlB;AACA,YAAME,cAAc,KAAKC,yBAAL,CAA+B,EAACZ,GAAD,EAAMtB,MAAM,WAAZ,EAA/B,CAApB;AACA,aAAOrB,kBAAkBsD,YAAYE,GAAZ,CAAgB5C,IAAhB,EAAsBgC,QAAtB,CAAlB,CAAP;AACD;;AAED;;;;;;;;;AASA,UAAMiD,MAAN,CAAajD,QAAb,EAAuB;AACrB,UAAI,CAACA,QAAL,EAAe;AACb,cAAM,IAAIxB,KAAJ,CAAU,iCAAV,CAAN;AACD;;AAED,YAAMuB,MAAM,MAAM,KAAKS,aAAL,EAAlB;AACA,YAAME,cAAc,KAAKC,yBAAL,CAA+B,EAACZ,GAAD,EAAMtB,MAAM,WAAZ,EAA/B,CAApB;AACA,aAAOrB,kBAAkBsD,YAAYwC,MAAZ,CAAmBlD,QAAnB,CAAlB,CAAP;AACD;;AAED;;;;;;;;;;;;AAYA,UAAMmD,IAAN,CAAWC,OAAX,EAAoB;AAClB,YAAMrD,MAAM,MAAM,KAAKS,aAAL,EAAlB;AACA,YAAME,cAAc,KAAKC,yBAAL,CAA+B,EAACZ,GAAD,EAA/B,CAApB;AACA,YAAMsD,UAAU,MAAMjG,kBAAkBsD,YAAY4C,UAAZ,EAAlB,CAAtB;;AAEA,UAAIC,eAAeF,OAAnB;;AAEA,UAAID,OAAJ,EAAa;AACXG,uBAAeA,aAAaC,MAAb,CAAoBC,OAAO;AACxC,cAAIC,QAAQ,IAAZ;;AAEA,cAAI,OAAON,QAAQO,UAAf,KAA8B,QAAlC,EAA4C;AAC1CD,oBAAQA,SAASD,IAAIE,UAAJ,CAAeP,QAAQO,UAAvB,CAAjB;AACD;;AAED,cAAI,OAAOP,QAAQQ,QAAf,KAA4B,QAAhC,EAA0C;AACxCF,oBAAQA,SAASD,IAAIG,QAAJ,CAAaR,QAAQQ,QAArB,CAAjB;AACD;;AAED,cAAI,OAAOR,QAAQS,QAAf,KAA4B,QAAhC,EAA0C;AACxCH,oBAAQA,SAASD,IAAII,QAAJ,CAAaT,QAAQS,QAArB,CAAjB;AACD;;AAED,cAAI,OAAOT,QAAQU,QAAf,KAA4B,UAAhC,EAA4C;AAC1CJ,oBAAQA,SAASN,QAAQU,QAAR,CAAiBL,GAAjB,CAAjB;AACD;;AAED,iBAAOC,KAAP;AACD,SApBc,CAAf;AAqBD;;AAED,aAAOH,YAAP;AACD;;AAED;;;;;;;;;;;;AAYA,UAAMQ,KAAN,CAAYX,OAAZ,EAAqB;AACnB,UAAI,CAACA,OAAL,EAAc;AACZ,cAAMrD,MAAM,MAAM,KAAKS,aAAL,EAAlB;AACA,cAAME,cAAc,KAAKC,yBAAL,CAA+B,EAACZ,GAAD,EAA/B,CAApB;AACA,eAAO3C,kBAAkBsD,YAAYqD,KAAZ,EAAlB,CAAP;AACD;;AAED,YAAMR,eAAe,MAAM,KAAKJ,IAAL,CAAUC,OAAV,CAA3B;AACA,aAAOG,aAAaS,MAApB;AACD;;AAED;;;;;;;;;AASA,UAAMlD,GAAN,CAAUd,QAAV,EAAoB;AAClB,YAAMD,MAAM,MAAM,KAAKS,aAAL,EAAlB;AACA,YAAME,cAAc,KAAKC,yBAAL,CAA+B,EAACZ,GAAD,EAA/B,CAApB;AACA,aAAO3C,kBAAkBsD,YAAYI,GAAZ,CAAgBd,QAAhB,CAAlB,EAA6CL,IAA7C,CAAkDhC,UAAU;AACjE,YAAI8D,OAAOW,cAAP,IAAyBzE,kBAAkB8D,OAAOW,cAAtD,EAAsE;AACpE,iBAAO,IAAIvC,sBAAJ,CAA2B;AAChCC,0BAAc,IADkB;AAEhCC,eAFgC;AAGhCC,oBAHgC;AAIhCC,sBAAUtC,OAAOsG,IAJe;AAKhC/D,yBAAavC;AALmB,WAA3B,CAAP;AAOD;;AAED,eAAOA,MAAP;AACD,OAZM,CAAP;AAaD;;AAED;;;;;;AAMA,UAAMuG,KAAN,GAAc;AACZ,YAAMnE,MAAM,MAAM,KAAKS,aAAL,EAAlB;AACA,YAAME,cAAc,KAAKC,yBAAL,CAA+B,EAACZ,GAAD,EAAMtB,MAAM,WAAZ,EAA/B,CAApB;AACA,aAAOrB,kBAAkBsD,YAAYwD,KAAZ,EAAlB,CAAP;AACD;AAhPyB;;UAAfpC,c,GAAAA,c;AAmPb;;;;;;;;;;;;;AAaO,iBAAezE,cAAf,CAA8B,EAAC4D,IAAD,EAAOc,UAAP,KAAqB,EAAnD,EAAuD;AAC5D,UAAMjC,eAAe,IAAIgC,cAAJ,CAAmB,EAACb,MAAMA,QAAQ,SAAf,EAA0Bc,UAA1B,EAAnB,CAArB;AACA,UAAMjC,aAAaU,aAAb,EAAN;AACA,WAAOV,YAAP;AACD;;AAED;;;;AAIA;;;;AAIA;;;;AAIA;;;;AAIA","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 @@
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+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/stored-credentials/README.md b/stored-credentials/README.md
new file mode 100644
index 0000000..b9d70f7
--- /dev/null
+++ b/stored-credentials/README.md
@@ -0,0 +1,32 @@
+# stored-credentials
+
+**Although this add-on uses a stored password to authenticate to a web server,
+it should not be taken as an example of how to store or work securely with
+passwords. It's only a demonstration of how to use the
+[`webRequest.onAuthRequired`](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/webRequest/onAuthRequired) API.**
+
+This add-on uses the [`webRequest.onAuthRequired`](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/webRequest/onAuthRequired) API to log the user into
+the demo site at https://httpbin.org/basic-auth/user/passwd using a stored
+username and password.
+
+This add-on stores a username and password using the [`storage.local`](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/storage/local) API.
+The default value is the correct value
+for the demo site:
+
+ username: "user"
+ password: "passwd"
+
+You can change the default values in the add-on's [options page](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Options_pages).
+
+The add-on then uses `webRequest.onAuthRequired` to intercept authentication
+requests from the demo site. When it gets
+such a request, it fetches the stored credentials and supplies them
+asynchronously.
+
+To try out the add-on:
+
+* Before installing the add-on, visit https://httpbin.org/basic-auth/user/passwd,
+and see that it asks for a username and password.
+* [Install the add-on](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Temporary_Installation_in_Firefox) in Firefox 54 or later.
+* Visit https://httpbin.org/basic-auth/user/passwd again, and see that authentication succeeds automatically.
+
diff --git a/stored-credentials/auth.js b/stored-credentials/auth.js
new file mode 100644
index 0000000..a5d4778
--- /dev/null
+++ b/stored-credentials/auth.js
@@ -0,0 +1,48 @@
+
+var target = "https://httpbin.org/basic-auth/*";
+
+var pendingRequests = [];
+
+/*
+A request has completed. We can stop worrying about it.
+*/
+function completed(requestDetails) {
+ console.log("completed: " + requestDetails.requestId);
+ var index = pendingRequests.indexOf(requestDetails.requestId);
+ if (index > -1) {
+ pendingRequests.splice(index, 1);
+ }
+}
+
+function provideCredentialsAsync(requestDetails) {
+ // If we have seen this request before,
+ // then assume our credentials were bad,
+ // and give up.
+ if (pendingRequests.indexOf(requestDetails.requestId) != -1) {
+ console.log("bad credentials for: " + requestDetails.requestId);
+ return {cancel: true};
+
+ } else {
+ pendingRequests.push(requestDetails.requestId);
+ console.log("providing credentials for: " + requestDetails.requestId);
+ // we can return a promise that will be resolved
+ // with the stored credentials
+ return browser.storage.local.get(null);
+ }
+}
+
+browser.webRequest.onAuthRequired.addListener(
+ provideCredentialsAsync,
+ {urls: [target]},
+ ["blocking"]
+ );
+
+browser.webRequest.onCompleted.addListener(
+ completed,
+ {urls: [target]}
+);
+
+browser.webRequest.onErrorOccurred.addListener(
+ completed,
+ {urls: [target]}
+);
diff --git a/stored-credentials/icons/LICENSE b/stored-credentials/icons/LICENSE
new file mode 100644
index 0000000..f39164e
--- /dev/null
+++ b/stored-credentials/icons/LICENSE
@@ -0,0 +1 @@
+The "lock".svg" icon is taken from the Material Core iconset and is used under the terms of its license: https://www.iconfinder.com/iconsets/material-core.
diff --git a/stored-credentials/icons/lock.svg b/stored-credentials/icons/lock.svg
new file mode 100644
index 0000000..1037a7d
--- /dev/null
+++ b/stored-credentials/icons/lock.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/stored-credentials/manifest.json b/stored-credentials/manifest.json
new file mode 100644
index 0000000..3e19a85
--- /dev/null
+++ b/stored-credentials/manifest.json
@@ -0,0 +1,31 @@
+{
+ "description": "Performs basic authentication by supplying stored credentials.",
+ "manifest_version": 2,
+ "name": "stored-credentials",
+ "version": "2.0",
+ "homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/stored-credentials",
+ "icons": {
+ "48": "icons/lock.svg"
+ },
+
+ "applications": {
+ "gecko": {
+ "strict_min_version": "54.0a1"
+ }
+ },
+
+ "background": {
+ "scripts": ["storage.js", "auth.js"]
+ },
+
+ "options_ui": {
+ "page": "options/options.html"
+ },
+
+ "permissions": [
+ "webRequest",
+ "webRequestBlocking",
+ "storage",
+ "https://httpbin.org/basic-auth/*"
+ ]
+}
diff --git a/stored-credentials/options/options.css b/stored-credentials/options/options.css
new file mode 100644
index 0000000..f54cbcf
--- /dev/null
+++ b/stored-credentials/options/options.css
@@ -0,0 +1,23 @@
+
+body {
+ width: 25em;
+ font-family: "Open Sans Light", sans-serif;
+ font-size: 0.9em;
+ font-weight: 300;
+}
+
+
+.title {
+ font-size: 1.2em;
+ margin-bottom: 0.5em;
+}
+
+label {
+ float: right;
+}
+
+input {
+ margin: 0.5em;
+ width: 200px;
+ height: 2.5em;
+}
diff --git a/stored-credentials/options/options.html b/stored-credentials/options/options.html
new file mode 100644
index 0000000..235dd6a
--- /dev/null
+++ b/stored-credentials/options/options.html
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
Username and password
+
+
+
+
+
+
+
+
+
+
diff --git a/stored-credentials/options/options.js b/stored-credentials/options/options.js
new file mode 100644
index 0000000..bb0477d
--- /dev/null
+++ b/stored-credentials/options/options.js
@@ -0,0 +1,39 @@
+const usernameInput = document.querySelector("#username");
+const passwordInput = document.querySelector("#password");
+
+/*
+Store the currently selected settings using browser.storage.local.
+*/
+function storeSettings() {
+ browser.storage.local.set({
+ authCredentials: {
+ username: usernameInput.value,
+ password: passwordInput.value
+ }
+ });
+}
+
+/*
+Update the options UI with the settings values retrieved from storage,
+or the default settings if the stored settings are empty.
+*/
+function updateUI(restoredSettings) {
+ usernameInput.value = restoredSettings.authCredentials.username || "";
+ passwordInput.value = restoredSettings.authCredentials.password || "";
+}
+
+function onError(e) {
+ console.error(e);
+}
+
+/*
+On opening the options page, fetch stored settings and update the UI with them.
+*/
+const gettingStoredSettings = browser.storage.local.get();
+gettingStoredSettings.then(updateUI, onError);
+
+/*
+On blur, save the currently selected settings.
+*/
+usernameInput.addEventListener("blur", storeSettings);
+passwordInput.addEventListener("blur", storeSettings);
diff --git a/stored-credentials/storage.js b/stored-credentials/storage.js
new file mode 100644
index 0000000..53cfa0a
--- /dev/null
+++ b/stored-credentials/storage.js
@@ -0,0 +1,27 @@
+/*
+Default settings. Initialize storage to these values.
+*/
+var authCredentials = {
+ username: "user",
+ password: "passwd"
+}
+
+/*
+Generic error logger.
+*/
+function onError(e) {
+ console.error(e);
+}
+
+/*
+On startup, check whether we have stored settings.
+If we don't, then store the default settings.
+*/
+function checkStoredSettings(storedSettings) {
+ if (!storedSettings.authCredentials) {
+ browser.storage.local.set({authCredentials});
+ }
+}
+
+const gettingStoredSettings = browser.storage.local.get();
+gettingStoredSettings.then(checkStoredSettings, onError);
diff --git a/tabs-tabs-tabs/manifest.json b/tabs-tabs-tabs/manifest.json
index 6877e80..76663af 100644
--- a/tabs-tabs-tabs/manifest.json
+++ b/tabs-tabs-tabs/manifest.json
@@ -1,17 +1,15 @@
{
- "applications": {
- "gecko": {
- "id": "tabs-tabs-tabs@mozilla.org",
- "strict_min_version": "47.0a1"
- }
- },
"browser_action": {
- "default_title": "Tabs, tabs, tabs",
- "default_popup": "tabs.html"
+ "browser_style": true,
+ "default_title": "Tabs, tabs, tabs",
+ "default_popup": "tabs.html"
},
"description": "A list of methods you can perform on a tab.",
"homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/tabs-tabs-tabs",
"manifest_version": 2,
"name": "Tabs, tabs, tabs",
+ "permissions": [
+ "tabs"
+ ],
"version": "1.0"
}
diff --git a/tabs-tabs-tabs/tabs.css b/tabs-tabs-tabs/tabs.css
index 8620216..bfd9822 100644
--- a/tabs-tabs-tabs/tabs.css
+++ b/tabs-tabs-tabs/tabs.css
@@ -1,3 +1,20 @@
html, body {
width: 350px;
}
+
+a {
+ margin: 10px;
+ display: inline-block;
+}
+
+.switch-tabs {
+ padding-left: 10px;
+}
+
+.switch-tabs a {
+ display: block;
+}
+
+.panel {
+ margin: 5px;
+}
diff --git a/tabs-tabs-tabs/tabs.html b/tabs-tabs-tabs/tabs.html
index afcf29a..81238cf 100644
--- a/tabs-tabs-tabs/tabs.html
+++ b/tabs-tabs-tabs/tabs.html
@@ -7,13 +7,40 @@
- Move active tab to the beginning of the window
- Move active tab to the end of the window
- Duplicate active tab
- Reload active tab
- Remove active tab
- Create a tab
- Alert active tab info
+
+
+
+
diff --git a/top-sites/sites.js b/top-sites/sites.js
new file mode 100644
index 0000000..f70573f
--- /dev/null
+++ b/top-sites/sites.js
@@ -0,0 +1,23 @@
+browser.topSites.get()
+ .then((sites) => {
+ var div = document.getElementById('site-list');
+
+ if (!sites.length) {
+ div.innerText = 'No sites returned from the topSites API.';
+ return;
+ }
+
+ let ul = document.createElement('ul');
+ ul.className = 'list-group';
+ for (let site of sites) {
+ let li = document.createElement('li');
+ li.className = 'list-group-item';
+ let a = document.createElement('a');
+ a.href = site.url;
+ a.innerText = site.title || site.url;
+ li.appendChild(a);
+ ul.appendChild(li);
+ }
+
+ div.appendChild(ul);
+ });
diff --git a/user-agent-rewriter/README.md b/user-agent-rewriter/README.md
index de0bd80..844afa8 100644
--- a/user-agent-rewriter/README.md
+++ b/user-agent-rewriter/README.md
@@ -2,9 +2,9 @@
## What it does
-This extension uses the webRequest API to rewrite the browser's User Agent header, but only when visiting pages under "http://useragentstring.com/".
+This extension uses the webRequest API to rewrite the browser's User Agent header, but only when visiting pages under "https://httpbin.org", for example: https://httpbin.org/user-agent
-It adds a browser action. The browser action has a popup that lets the user choose one of three browsers: Firefox 41, Chrome 41, and IE 11. When the user chooses a browser, the extension then rewrites the User Agent header so the real browser identifies itself as the chosen browser on the site http://useragentstring.com/.
+It adds a browser action. The browser action has a popup that lets the user choose one of three browsers: Firefox 41, Chrome 41, and IE 11. When the user chooses a browser, the extension then rewrites the User Agent header so the real browser identifies itself as the chosen browser on the site https://httpbin.org/.
## What it shows
diff --git a/user-agent-rewriter/background.js b/user-agent-rewriter/background.js
index 4b2e347..c345a08 100644
--- a/user-agent-rewriter/background.js
+++ b/user-agent-rewriter/background.js
@@ -3,7 +3,7 @@
/*
This is the page for which we want to rewrite the User-Agent header.
*/
-var targetPage = "http://useragentstring.com/*";
+var targetPage = "https://httpbin.org/*";
/*
Map browser names to UA strings.
@@ -24,7 +24,7 @@ Rewrite the User-Agent header to "ua".
*/
function rewriteUserAgentHeader(e) {
for (var header of e.requestHeaders) {
- if (header.name == "User-Agent") {
+ if (header.name.toLowerCase() === "user-agent") {
header.value = ua;
}
}
@@ -37,7 +37,7 @@ only for the target page.
Make it "blocking" so we can modify the headers.
*/
-chrome.webRequest.onBeforeSendHeaders.addListener(rewriteUserAgentHeader,
+browser.webRequest.onBeforeSendHeaders.addListener(rewriteUserAgentHeader,
{urls: [targetPage]},
["blocking", "requestHeaders"]);
diff --git a/user-agent-rewriter/manifest.json b/user-agent-rewriter/manifest.json
index 698ea1e..f2bb5a2 100644
--- a/user-agent-rewriter/manifest.json
+++ b/user-agent-rewriter/manifest.json
@@ -9,15 +9,8 @@
"48": "icons/person-48.png"
},
- "applications": {
- "gecko": {
- "id": "user-agent-rewriter@mozilla.org",
- "strict_min_version": "45.0"
- }
- },
-
"permissions": [
- "webRequest", "webRequestBlocking", "http://useragentstring.com/*"
+ "webRequest", "webRequestBlocking", "https://httpbin.org/*"
],
"background": {
diff --git a/user-agent-rewriter/popup/choose_ua.js b/user-agent-rewriter/popup/choose_ua.js
index 0a1869b..91dbbc9 100644
--- a/user-agent-rewriter/popup/choose_ua.js
+++ b/user-agent-rewriter/popup/choose_ua.js
@@ -4,12 +4,12 @@ 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;
}
var chosenUa = e.target.textContent;
- var backgroundPage = chrome.extension.getBackgroundPage();
+ var backgroundPage = browser.extension.getBackgroundPage();
backgroundPage.setUaString(chosenUa);
});
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/webpack-modules/README.md b/webpack-modules/README.md
new file mode 100644
index 0000000..eaf5d1b
--- /dev/null
+++ b/webpack-modules/README.md
@@ -0,0 +1,47 @@
+# WebExtension Webpack Example
+A minimal example of how to use [webpack](https://webpack.github.io) to package
+[npm](https://npmjs.com) modules so they can be used in a WebExtension.
+The example package used by this extension is `left-pad`, an essential package
+in almost any situation.
+
+## What it does
+This example shows how to use a node module in a background and a content script.
+It defines two build targets in [webpack.config.js](webpack.config.js), they each
+generate a file that includes all modules used the entry point and store it in
+the [addon](addon/) folder. The first one starts with [background_scripts/background.js](background_scripts/background.js)
+and stores it in `addon/background_scripts/index.js`. The other one does the
+same for [popup/left-pad.js](popup/left-pad.js) and stores it in `addon/popup/index.js`.
+
+The extension includes a browser action with a popup, which provides an UI for
+running left-pad on a string with a chosen character. The operation can either be
+performed with the left-pad module included in the panel's script or in the
+background script.
+
+## What it could do
+This could be infinitely extended - injecting global jQuery, adding babel,
+react/jsx, css modules, image processing, local modules and so on.
+
+## What it shows
+
+ - How to use npm or custom modules in a WebExtension.
+
+## How to build it
+
+ - `npm install`
+ - `npm run build`
+
+The WebExtension in the [addon](addon/) folder should now work.
+
+## What about Browserify?
+[Browserify](http://browserify.org/) works just as well as webpack for extensions. In the end it's a
+personal choice about your preferred tool.
+
+## Live-development
+As well as watching the folder with your `manifest.json` in it, you will also
+have to run webpack in watch mode. You can use the
+[webpack-webext-plugin](https://github.com/rpl/webpack-webext-plugin) to simplify the workflow.
+
+## On addons.mozilla.org Reviews
+Files generated by webpack and friends are compiled files. You have to [upload the source](https://developer.mozilla.org/en-US/Add-ons/AMO/Policy/Reviews#Source_Code_Submission) you generated your extension from to AMO for review of a listed extension. This will mean that your extension has to be reviewed by an admin reviewer, which will result in a longer wait time in queue.
+
+To make the review easier, you can exclude third-party libraries from your output and directly ship the original distribution files for the libraries. This allows AMO to automatically recognize libraries and mark them as safe. This can be achieved with [externals](https://webpack.js.org/configuration/externals/) in the configuration for webpack.
diff --git a/webpack-modules/addon/icons/leftpad-32.png b/webpack-modules/addon/icons/leftpad-32.png
new file mode 100644
index 0000000..4987e78
Binary files /dev/null and b/webpack-modules/addon/icons/leftpad-32.png differ
diff --git a/webpack-modules/addon/manifest.json b/webpack-modules/addon/manifest.json
new file mode 100644
index 0000000..62ae79f
--- /dev/null
+++ b/webpack-modules/addon/manifest.json
@@ -0,0 +1,18 @@
+{
+ "manifest_version": 2,
+ "name": "Webpack Example",
+ "version": "1.0.0",
+ "description": "A minimal example of how to use npm modules from within a WebExtension.",
+ "icons": {
+ "32": "icons/leftpad-32.png"
+ },
+ "browser_action": {
+ "default_icon": "icons/leftpad-32.png",
+ "default_title": "Left Pad",
+ "default_popup": "popup/left-pad.html",
+ "browser_style": true
+ },
+ "background": {
+ "scripts": ["background_scripts/index.js"]
+ }
+}
diff --git a/webpack-modules/addon/popup/left-pad.html b/webpack-modules/addon/popup/left-pad.html
new file mode 100644
index 0000000..8fb2e2a
--- /dev/null
+++ b/webpack-modules/addon/popup/left-pad.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/webpack-modules/addon/popup/style.css b/webpack-modules/addon/popup/style.css
new file mode 100644
index 0000000..5b1c6aa
--- /dev/null
+++ b/webpack-modules/addon/popup/style.css
@@ -0,0 +1,46 @@
+/*
+ * These styles extend the styles from browser_styles, see https://firefoxux.github.io/StyleGuide.
+ */
+
+/* For some reason the footer creates a horizontal overflow */
+body {
+ overflow-x: hidden;
+}
+
+.panel-formElements-item label {
+ width: 80px;
+}
+
+.panel-formElements-item output,
+.panel-formElements-item input[type="number"] {
+ flex-grow: 1;
+}
+
+input[type="number"] {
+ background-color: #fff;
+ border: 1px solid #b1b1b1;
+ box-shadow: 0 0 0 0 rgba(97, 181, 255, 0);
+ font: caption;
+ padding: 0 6px 0;
+ transition-duration: 250ms;
+ transition-property: box-shadow;
+ height: 24px;
+}
+
+input[type="number"]:hover {
+ border-color: #858585;
+}
+
+input[type="number"]:focus {
+ border-color: #0996f8;
+ box-shadow: 0 0 0 2px rgba(97, 181, 255, 0.75);
+}
+
+/* Reset the default styles for buttons if it's a footer button */
+button.panel-section-footer-button {
+ padding: 12px;
+ border: none;
+ margin: 0;
+ box-shadow: none;
+ background-color: none;
+}
diff --git a/webpack-modules/background_scripts/background.js b/webpack-modules/background_scripts/background.js
new file mode 100644
index 0000000..7af7653
--- /dev/null
+++ b/webpack-modules/background_scripts/background.js
@@ -0,0 +1,6 @@
+const leftPad = require("left-pad");
+
+browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
+ const result = leftPad(message.text, message.amount, message.with);
+ sendResponse(result);
+});
diff --git a/webpack-modules/package.json b/webpack-modules/package.json
new file mode 100644
index 0000000..7031cad
--- /dev/null
+++ b/webpack-modules/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "webpack-webextension",
+ "version": "1.0.0",
+ "description": "A minimal example of how to use npm modules from within a WebExtension.",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "build": "webpack"
+ },
+ "license": "MPL-2.0",
+ "devDependencies": {
+ "webpack": "^2.3.2"
+ },
+ "dependencies": {
+ "left-pad": "^1.1.1"
+ }
+}
diff --git a/webpack-modules/popup/left-pad.js b/webpack-modules/popup/left-pad.js
new file mode 100644
index 0000000..766d1a4
--- /dev/null
+++ b/webpack-modules/popup/left-pad.js
@@ -0,0 +1,24 @@
+const leftPad = require("left-pad");
+
+const resultNode = document.getElementById("result");
+const textNode = document.getElementById("text");
+const amountNode = document.getElementById("amount");
+const withNode = document.getElementById("with");
+
+document.getElementById("leftpad-form").addEventListener("submit", (e) => {
+ e.preventDefault();
+
+ console.log("padding");
+ resultNode.value = leftPad(textNode.value, amountNode.valueAsNumber, withNode.value);
+}, false);
+
+document.getElementById("pad-bg").addEventListener("click", (e) => {
+ var sendingMessage = browser.runtime.sendMessage({
+ text: textNode.value,
+ amount: amountNode.valueAsNumber,
+ with: withNode.value
+ });
+ sendingMessage.then((result) => {
+ resultNode.value = result;
+ });
+});
diff --git a/webpack-modules/webpack.config.js b/webpack-modules/webpack.config.js
new file mode 100644
index 0000000..e19012a
--- /dev/null
+++ b/webpack-modules/webpack.config.js
@@ -0,0 +1,12 @@
+const path = require("path");
+
+module.exports = {
+ entry: {
+ background_scripts: "./background_scripts/background.js",
+ popup: "./popup/left-pad.js"
+ },
+ output: {
+ path: path.resolve(__dirname, "addon"),
+ filename: "[name]/index.js"
+ }
+};
diff --git a/window-manipulator/README.md b/window-manipulator/README.md
new file mode 100644
index 0000000..46d37cf
--- /dev/null
+++ b/window-manipulator/README.md
@@ -0,0 +1,11 @@
+# Window manipulator
+
+## What it does
+
+This extension includes a browser action with a popup specified as "window.html".
+
+The popup lets the user perform various simple operations using the windows API.
+
+# What it shows
+
+Demonstration of various windows API functions.
diff --git a/window-manipulator/icons/window.png b/window-manipulator/icons/window.png
new file mode 100644
index 0000000..e707a11
Binary files /dev/null and b/window-manipulator/icons/window.png differ
diff --git a/window-manipulator/icons/window19.png b/window-manipulator/icons/window19.png
new file mode 100644
index 0000000..8207534
Binary files /dev/null and b/window-manipulator/icons/window19.png differ
diff --git a/window-manipulator/icons/window38.png b/window-manipulator/icons/window38.png
new file mode 100644
index 0000000..b42ab1e
Binary files /dev/null and b/window-manipulator/icons/window38.png differ
diff --git a/window-manipulator/icons/window@2x.png b/window-manipulator/icons/window@2x.png
new file mode 100644
index 0000000..9a5044e
Binary files /dev/null and b/window-manipulator/icons/window@2x.png differ
diff --git a/window-manipulator/manifest.json b/window-manipulator/manifest.json
new file mode 100644
index 0000000..5963b9b
--- /dev/null
+++ b/window-manipulator/manifest.json
@@ -0,0 +1,20 @@
+{
+ "browser_action": {
+ "browser_style": true,
+ "default_title": "Window manipulator",
+ "default_popup": "window.html",
+ "default_icon": {
+ "19": "icons/window19.png",
+ "38": "icons/window38.png"
+ }
+ },
+ "icons": {
+ "48": "icons/window.png",
+ "96": "icons/window@2x.png"
+ },
+ "description": "A list of methods you can perform on a window.",
+ "homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/window-manipulator",
+ "manifest_version": 2,
+ "name": "Window manipulator",
+ "version": "1.0"
+}
diff --git a/window-manipulator/window.css b/window-manipulator/window.css
new file mode 100644
index 0000000..959491f
--- /dev/null
+++ b/window-manipulator/window.css
@@ -0,0 +1,12 @@
+html, body {
+ width: 350px;
+}
+
+a {
+ margin: 10px;
+ display: inline-block;
+}
+
+.panel {
+ margin: 5px;
+}
diff --git a/window-manipulator/window.html b/window-manipulator/window.html
new file mode 100644
index 0000000..f1ebd95
--- /dev/null
+++ b/window-manipulator/window.html
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+