Merge remote-tracking branch 'origin/master' into beastify-injection-fixes

* origin/master: (26 commits)
  convert chrome. to browser. Issue #165 #166 (#262)
  Update examples.json for contextMenus->menus change (#282)
  Update context-menu-demo (#272)
  change shortcut to Ctrl+Shift+U for commands example (#264)
  Update `proxy-blocker` extension to be compatible with firefox 56+ (#260)
  Update eslint and .travis.yml (#259)
  add in titlePreface (#256)
  Hellosct1 webext (#237)
  Remove 'highlight', as Firefox does not support it (#252)
  Reflect bookmark state in icon title to make it accessible to screen readers (#255)
  Adding listing/descriptions for the imagify and themes examples. (#253)
  Selfify example (#251)
  New theme examples (#248)
  Example: embedded webextension overlay (#249)
  Add an indexedDB file storage example: image-reference-collector (#224)
  webextension -> extension (#250)
  more fields into package.json
  Add travis-ci build status badge
  Add .travis.yml
  more .eslintrc.json and fixes
  ...
This commit is contained in:
Will Bamberg
2017-09-11 17:17:33 -07:00
140 changed files with 2730 additions and 116 deletions

4
.eslintignore Normal file
View File

@@ -0,0 +1,4 @@
**/node_modules/**
react-es6-popup/**/dist
mocha-client-tests
store-collected-images/webextension-plain/deps

22
.eslintrc.json Normal file
View File

@@ -0,0 +1,22 @@
{
"root": true,
"parserOptions": {
"ecmaVersion": 6
},
"env": {
"browser": true,
"es6": true,
"webextensions": true
},
"extends": [
"eslint:recommended"
],
"rules": {
"no-console": 0,
"no-unused-vars": ["warn", { "vars": "all", "args": "all" } ],
"no-undef": ["warn"],
"no-proto": ["error"],
"prefer-arrow-callback": ["warn"],
"prefer-spread": ["warn"]
}
}

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules

3
.travis.yml Normal file
View File

@@ -0,0 +1,3 @@
language: node_js
node_js: stable
sudo: false

View File

@@ -1,4 +1,4 @@
# webextensions-examples
# webextensions-examples [![Build Status](https://travis-ci.org/mdn/webextensions-examples.svg?branch=master)](https://travis-ci.org/mdn/webextensions-examples)
[https://github.com/mdn/webextensions-examples](https://github.com/mdn/webextensions-examples)

View File

@@ -4,14 +4,14 @@ const contentBox = document.querySelector("#content");
/*
Make the content box editable as soon as the user mouses over the sidebar.
*/
window.addEventListener("mouseover", (e) => {
window.addEventListener("mouseover", () => {
contentBox.setAttribute("contenteditable", true);
});
/*
When the user mouses out, save the current contents of the box.
*/
window.addEventListener("mouseout", (e) => {
window.addEventListener("mouseout", () => {
contentBox.setAttribute("contenteditable", false);
browser.tabs.query({windowId: myWindowId, active: true}).then((tabs) => {
let contentToStore = {};

View File

@@ -51,7 +51,7 @@ When first loaded, initialize the page action for all tabs.
*/
var gettingAllTabs = browser.tabs.query({});
gettingAllTabs.then((tabs) => {
for (tab of tabs) {
for (let tab of tabs) {
initializePageAction(tab);
}
});

View File

@@ -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
});
}
/*

View File

@@ -69,6 +69,6 @@ browser.alarms.onAlarm.addListener((alarm) => {
/*
On page action click, navigate the corresponding tab to the cat gifs.
*/
browser.pageAction.onClicked.addListener(function () {
browser.pageAction.onClicked.addListener(() => {
browser.tabs.update({url: CATGIFS});
});

View File

@@ -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.

View File

@@ -12,7 +12,7 @@
*/
var gettingAllCommands = browser.commands.getAll();
gettingAllCommands.then((commands) => {
for (command of commands) {
for (let command of commands) {
console.log(command);
}
});

View File

@@ -3,7 +3,7 @@ browser.contextMenus.create({
title: "Copy link to clipboard",
contexts: ["link"],
});
browser.contextMenus.onClicked.addListener(function(info, tab) {
browser.contextMenus.onClicked.addListener((info, tab) => {
if (info.menuItemId === "copy-link-to-clipboard") {
// Examples: text and HTML to be copied.
const text = "This is text: " + info.linkUrl;
@@ -22,7 +22,7 @@ browser.contextMenus.onClicked.addListener(function(info, tab) {
browser.tabs.executeScript({
code: "typeof copyToClipboard === 'function';",
}).then(function(results) {
}).then((results) => {
// The content script's last expression will be true if the function
// has been defined. If this is not the case, then we need to run
// clipboard-helper.js to define function copyToClipboard.
@@ -31,11 +31,11 @@ browser.contextMenus.onClicked.addListener(function(info, tab) {
file: "clipboard-helper.js",
});
}
}).then(function() {
}).then(() => {
return browser.tabs.executeScript(tab.id, {
code,
});
}).catch(function(error) {
}).catch((error) => {
// This could happen if the extension is not allowed to run code in
// the page, for example if the tab is a privileged page.
console.error("Failed to copy text: " + error);

29
devtools-panels/README.md Normal file
View File

@@ -0,0 +1,29 @@
# devtools-panels
**Adds a new panel to the developer tools. The panel contains buttons that demonstrate various basic features of the devtools API.**
## What it does ##
This extension adds a new panel to the developer tools. The panel contains four buttons:
* **Inspect H1**: this injects a script into the active page. The script uses the [`inspect()` helper function](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/devtools.inspectedWindow/eval#Helpers) to select the first <h1> element in the page in the devtools inspector.
* **Reddinate inspected element**: this injects a script into the active page. The script uses the [`$0` helper](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/devtools.inspectedWindow/eval#Helpers) to get the element that's currently selected in the devtools Inspector, and gives it a red background.
* **Check for jQuery**: this injects a script into the active page. The script checks whether `jQuery` is defined in the page, and logs a string to the add-on debugging console (note: *not* the web console) recording the result.
* **Inject content script**: this sends a message to the extension's background script, asking it to inject a given content script in the active page.
To learn more about the devtools APIs, see [Extending the developer tools](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Extending_the_developer_tools).
## What it shows ##
* How to add a new panel to the devtools.
* How to inject a script into the active page using [`inspectedWindow.eval()`](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/devtools.inspectedWindow/eval).
* How to use [helpers](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/devtools.inspectedWindow/eval#Helpers) to interact with the devtools.
* That unlike content scripts, scripts injected with `eval()` can see objects, like `jQuery`, that were added by page scripts.
* How to send messages to the background script.

View File

@@ -0,0 +1,23 @@
/**
When we receive the message, execute the given script in the given
tab.
*/
function handleMessage(request, sender, sendResponse) {
if (sender.url != browser.runtime.getURL("/devtools/panel/panel.html")) {
return;
}
browser.tabs.executeScript(
request.tabId,
{
code: request.script
});
}
/**
Listen for messages from our devtools panel.
*/
browser.runtime.onMessage.addListener(handleMessage);

View File

@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<script src="devtools.js"></script>
</body>
</html>

View File

@@ -0,0 +1,24 @@
/**
This script is run whenever the devtools are open.
In here, we can create our panel.
*/
function handleShown() {
console.log("panel is being shown");
}
function handleHidden() {
console.log("panel is being hidden");
}
/**
Create a panel, and add listeners for panel show/hide events.
*/
browser.devtools.panels.create(
"My Panel",
"icons/star.png",
"devtools/panel/panel.html"
).then((newPanel) => {
newPanel.onShown.addListener(handleShown);
newPanel.onHidden.addListener(handleHidden);
});

View File

@@ -0,0 +1,72 @@
/**
Handle errors from the injected script.
Errors may come from evaluating the JavaScript itself
or from the devtools framework.
See https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/devtools.inspectedWindow/eval#Return_value
*/
function handleError(error) {
if (error.isError) {
console.log(`Devtools error: ${error.code}`);
} else {
console.log(`JavaScript error: ${error.value}`);
}
}
/**
Handle the result of evaluating the script.
If there was an error, call handleError.
*/
function handleResult(result) {
if (result[1]) {
handleError(result[1]);
}
}
/**
Handle the result of evaluating the jQuery test script.
Log the result of the test, or
if there was an error, call handleError.
*/
function handlejQueryResult(result) {
if (result[0] !== undefined) {
console.log(`jQuery: ${result[0]}`);
} else if (result[1]) {
handleError(result[1]);
}
}
/**
When the user clicks the 'jquery' button,
evaluate the jQuery script.
*/
const checkjQuery = "typeof jQuery != 'undefined'";
document.getElementById("button_jquery").addEventListener("click", () => {
browser.devtools.inspectedWindow.eval(checkjQuery)
.then(handlejQueryResult);
});
/**
When the user clicks each of the first three buttons,
evaluate the corresponding script.
*/
const evalString = "$0.style.backgroundColor = 'red'";
document.getElementById("button_background").addEventListener("click", () => {
browser.devtools.inspectedWindow.eval(evalString)
.then(handleResult);
});
const inspectString = "inspect(document.querySelector('h1'))";
document.getElementById("button_h1").addEventListener("click", () => {
browser.devtools.inspectedWindow.eval(inspectString)
.then(handleResult);
});
/**
When the user clicks the 'message' button,
send a message to the background script.
*/
const scriptToAttach = "document.body.innerHTML = 'Hi from the devtools';";
document.getElementById("button_message").addEventListener("click", () => {
browser.runtime.sendMessage({
tabId: browser.devtools.inspectedWindow.tabId,
script: scriptToAttach
});
});

View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<button id="button_h1">Inspect h1</button>
<button id="button_background">Reddinate inspected element</button>
<button id="button_jquery">Check for jquery</button>
<button id="button_message">Inject content script</button>
<script src="devtools-panel.js"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,22 @@
{
"description": "Adds a new panel to the developer tools. The panel contains buttons that demonstrate various basic features of the devtools API.",
"manifest_version": 2,
"name": "devtools-panels",
"version": "1.0",
"author": "Christophe Villeneuve",
"homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/devtools-panels",
"icons": {
"48": "icons/star.png"
},
"background": {
"scripts": ["background_scripts/background.js"]
},
"permissions": [
"<all_urls>"
],
"devtools_page": "devtools/devtools-page.html"
}

View File

@@ -0,0 +1,8 @@
{
"env": {
"browser": true,
"es6": true,
"amd": true,
"webextensions": true
}
}

View File

@@ -0,0 +1,2 @@
content my-overlay-addon content/
overlay chrome://browser/content/browser.xul chrome://my-overlay-addon/content/overlay.xul

View File

@@ -0,0 +1,31 @@
/* globals Components, dump */
{
const addonId = "my-overlay-addon@me";
const {
AddonManager,
} = Components.utils.import("resource://gre/modules/AddonManager.jsm", {});
AddonManager.getAddonByID(addonId, addon => {
const baseURI = addon.getResourceURI("/");
const {
LegacyExtensionsUtils,
} = Components.utils.import("resource://gre/modules/LegacyExtensionsUtils.jsm");
const myOverlayEmbeddedWebExtension = LegacyExtensionsUtils.getEmbeddedExtensionFor({
id: addonId, resourceURI: baseURI,
});
myOverlayEmbeddedWebExtension.startup().then(({browser}) => {
dump(`${addonId} - embedded webext started\n`);
browser.runtime.onMessage.addListener(msg => {
dump(`${addonId} - received message from embedded webext ${msg}\n`);
});
}).catch(err => {
Components.utils.reportError(
`${addonId} - embedded webext startup failed: ${err.message} ${err.stack}\n`
);
});
});
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0"?>
<overlay id="myOverlayAddon" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script src="chrome://my-overlay-addon/content/init.js"></script>
</overlay>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:id>my-overlay-addon@me</em:id>
<em:version>1.0.1</em:version>
<em:name>My Legacy Overlay Addon</em:name>
<em:type>2</em:type>
<em:multiprocessCompatible>true</em:multiprocessCompatible>
<em:targetApplication>
<Description>
<!-- firefox -->
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
<em:minVersion>51.0</em:minVersion>
<em:maxVersion>*</em:maxVersion>
</Description>
</em:targetApplication>
</Description>
</RDF>

View File

@@ -0,0 +1,3 @@
console.log("Embedded WebExtension", window.location.href);
browser.runtime.sendMessage("embedded_webext -> overlay addon container");

View File

@@ -0,0 +1,10 @@
{
"manifest_version": 2,
"name": "Overlay Addon WebExtension",
"version": "1.0.1",
"description": "test embedding a webextension in a legacy overlay addon",
"background": {
"scripts": ["background.js"]
}
}

View File

@@ -0,0 +1,8 @@
{
"env": {
"browser": true,
"es6": true,
"amd": true,
"webextensions": true
}
}

View File

@@ -2,6 +2,8 @@
* This file contains the Map of word --> emoji substitutions.
*/
/* exported sortedEmojiMap */
let dictionary = new Map();
dictionary.set('apple', '🍎');
dictionary.set('banana', '🍌');

View File

@@ -3,6 +3,8 @@
* all occurrences of each mapped word with its emoji counterpart.
*/
/*global sortedEmojiMap*/
// emojiMap.js defines the 'sortedEmojiMap' variable.
// Referenced here to reduce confusion.
const emojiMap = sortedEmojiMap;

View File

@@ -82,12 +82,12 @@
"commands.onCommand"
],
"name": "commands",
"description": "Demonstrates using the commands API to set up a keyboard shortcut. The shortcut created is accessed using Ctrl+Shift+Y (Command+Shift+Y on a Mac)."
"description": "Demonstrates using the commands API to set up a keyboard shortcut. The shortcut created is accessed using Ctrl+Shift+U (Command+Shift+U on a Mac)."
},
{
"javascript_apis": [
"contextMenus.create",
"contextMenus.onClicked",
"menus.create",
"menus.onClicked",
"tabs.executeScript"
],
"name": "context-menu-copy-link-with-types",
@@ -95,16 +95,16 @@
},
{
"javascript_apis": [
"contextMenus.create",
"contextMenus.onClicked",
"contextMenus.remove",
"contextMenus.update",
"menus.create",
"menus.onClicked",
"menus.remove",
"menus.update",
"i18n.getMessage",
"runtime.lastError",
"tabs.executeScript"
],
"name": "context-menu-demo",
"description": "Demonstrates adding and manipulating context menu items using the contextMenus API."
"name": "menu-demo",
"description": "Demonstrates adding and manipulating menu items using the menus API."
},
{
"javascript_apis": [
@@ -170,7 +170,7 @@
{
"javascript_apis": [],
"name": "eslint-example",
"description": "Demonstrates how to configure a WebExtension with eslint."
"description": "Demonstrates how to configure an extension with eslint."
},
{
"javascript_apis": [
@@ -223,6 +223,16 @@
"name": "history-deleter",
"description": "History API demo: deletes history items for a given domain"
},
{
"javascript_apis": [
"runtime.onMessage",
"tabs.executeScript",
"tabs.query",
"tabs.sendMessage"
],
"name": "imagify",
"description": "Using a sidebar, illustrates the use of file picker and drag and drop. A content script replaces the current page content with the chosen image."
},
{
"javascript_apis": [
"downloads.erase",
@@ -247,7 +257,7 @@
"runtime.sendMessage"
],
"name": "mocha-client-tests",
"description": "This example shows two methods of testing a WebExtension: running tests from within the add-on, and running tests from the command line using Karma"
"description": "This example shows two methods of testing an extension: running tests from within the extension, and running tests from the command line using Karma"
},
{
"javascript_apis": [
@@ -255,7 +265,7 @@
"runtime.connectNative"
],
"name": "native-messaging",
"description": "Example of native messaging, including a Python application and a WebExtension which exchanges messages with it."
"description": "Example of native messaging, including a Python application and an extension which exchanges messages with it."
},
{
"javascript_apis": [
@@ -305,7 +315,7 @@
"javascript_apis": [
"extension.getURL",
"proxy.onProxyError",
"proxy.registerProxyScript",
"proxy.register",
"runtime.onMessage",
"runtime.sendMessage",
"storage.local",
@@ -366,6 +376,12 @@
"name": "theme-switcher",
"description": "An example of how to use the management API for themes."
},
{
"javascript_apis": [
],
"name": "themes",
"description": "A collection of themes illustrating:<ul><li>weta_fade: a basic theme employing a single image specified in <code>headerURL:</code>.</li><li>weta_fade_chrome: the weta_fade theme implemented with Chrome compatible manifest keys.</li><li>weta_tiled: a theme using a tiled image.</li><li>weta_mirror: a theme using multiple images and aligning those images in the header.</li><li>animated: use of an animated PNG.</li></ul>"
},
{
"javascript_apis": [
"topSites.get"
@@ -387,7 +403,7 @@
"runtime.sendMessage"
],
"name": "webpack-modules",
"description": "Demonstrates how to use webpack to package npm modules in a WebExtension."
"description": "Demonstrates how to use webpack to package npm modules in an extension."
},
{
"javascript_apis": [

View File

@@ -1,3 +1,5 @@
/* exported getAccessToken */
const REDIRECT_URL = browser.identity.getRedirectURL();
const CLIENT_ID = "YOUR-CLIENT-ID";
const SCOPES = ["openid", "email", "profile"];
@@ -10,7 +12,7 @@ const AUTH_URL =
const VALIDATION_BASE_URL="https://www.googleapis.com/oauth2/v3/tokeninfo";
function extractAccessToken(redirectUri) {
let m = redirectUri.match(/[#\?](.*)/);
let m = redirectUri.match(/[#?](.*)/);
if (!m || m.length < 1)
return null;
let params = new URLSearchParams(m[1].split("#")[0]);

View File

@@ -1,3 +1,5 @@
/*global getAccessToken*/
function notifyUser(user) {
browser.notifications.create({
"type": "basic",

View File

@@ -2,6 +2,9 @@
Fetch the user's info, passing in the access token in the Authorization
HTTP request header.
*/
/* exported getUserInfo */
function getUserInfo(accessToken) {
const requestURL = "https://www.googleapis.com/oauth2/v1/userinfo?alt=json";
const requestHeaders = new Headers();

View File

@@ -7,7 +7,7 @@ function get_hostname(url) {
}
function set_domain(domain) {
spans = document.getElementsByClassName('domain');
const spans = document.getElementsByClassName('domain');
[].slice.call(spans).forEach((span) => {
span.textContent = domain;
});
@@ -65,7 +65,7 @@ function clearAll(e) {
// Loop through them and delete them one by one.
var searchingHistory = browser.history.search({text: hostname})
searchingHistory.then((results) => {
for (k = 0; k < results.length; k++) {
for (let k of results) {
browser.history.deleteUrl({url: results[k].url});
}
// Clear out the UI.

26
imagify/README.md Normal file
View File

@@ -0,0 +1,26 @@
# imagify
**This add-on injects JavaScript into web pages. The `addons.mozilla.org` domain disallows this operation, so this add-on will not work properly when it's run on pages in the `addons.mozilla.org` domain.**
## What it does ##
The extension includes:
* a sidebar including HTML, CSS, and JavaScript
* a content script
* a web page template, packaged as web accessible resources
When the extension loads the user is offered a file picker and drop zone as methods to choose an image file.
Once an image file has been chosen, the extension injects the content script into the active tab, and sends the content script a message containing the URL of the chosen image.
When the content script receives this message, it replaces the current page content with the chosen image file.
## What it shows ##
How to:
* write a sidebar
* implement a file picker and drag and drop zone
* give a sidebar style and behavior using CSS and JavaScript
* inject a content script programmatically using `tabs.executeScript()`
* send a message from the main extension to a content script

View File

@@ -0,0 +1,47 @@
(function() {
/*
Check and set a global guard variable.
If this content script is injected into the same page again,
it will do nothing next time.
*/
if (window.hasRun) {
return;
}
window.hasRun = true;
/*
Add the image to the web page by:
* Removing every node in the document.body
* Inserting the selected image
*/
function injectImage(request, sender, sendResponse) {
removeEverything();
insertImage(request.imageURL);
}
/*
Remove every node under document.body
*/
function removeEverything() {
while (document.body.firstChild) {
document.body.firstChild.remove();
}
}
/*
Given a URL to an image, create and style an iframe containing an
IMG node pointing to that image, then insert the node into the document.
*/
function insertImage(imageURL) {
const insertImage = document.createElement("iframe");
insertImage.setAttribute("src", browser.extension.getURL(`/viewer.html?blobURL=${imageURL}`));
insertImage.setAttribute("style", "width: 100vw; height: 100vh;");
document.body.appendChild(insertImage);
}
/*
Assign injectImage() as a listener for messages from the extension.
*/
browser.runtime.onMessage.addListener(injectImage);
})();

22
imagify/manifest.json Normal file
View File

@@ -0,0 +1,22 @@
{
"description": "Adds a sidebar offerin a file picker and drap and drop zone. When an image file is chosen the active tab's body content is replaced with file selected. See https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Examples#imagify",
"manifest_version": 2,
"name": "Imagify",
"version": "1.0",
"homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/imagify",
"permissions": [
"tabs",
"<all_urls>"
],
"sidebar_action": {
"default_title": "Imagify",
"default_panel": "sidebar/sidebar.html"
},
"web_accessible_resources": [
"/viewer.html"
]
}

View File

@@ -0,0 +1,63 @@
/*
Listens for a file being selected, creates a ObjectURL for the chosen file, injects a
content script into the active tab then passes the image URL through a message to the
active tab ID.
*/
// Listen for a file being selected through the file picker
const inputElement = document.getElementById("input");
inputElement.addEventListener("change", handlePicked, false);
// Listen for a file being dropped into the drop zone
const dropbox = document.getElementById("drop_zone");
dropbox.addEventListener("dragenter", dragenter, false);
dropbox.addEventListener("dragover", dragover, false);
dropbox.addEventListener("drop", drop, false);
// Get the image file if it was chosen from the pick list
function handlePicked() {
displayFile(this.files);
}
// Get the image file if it was dragged into the sidebar drop zone
function drop(e) {
e.stopPropagation();
e.preventDefault();
displayFile(e.dataTransfer.files);
}
/*
Insert the content script and send the image file ObjectURL to the content script using a
message.
*/
function displayFile(fileList) {
const imageURL = window.URL.createObjectURL(fileList[0]);
browser.tabs.executeScript({
file: "/content_scripts/content.js"
}).then(messageContent)
.catch(reportError);
function messageContent() {
const gettingActiveTab = browser.tabs.query({active: true, currentWindow: true});
gettingActiveTab.then((tabs) => {
browser.tabs.sendMessage(tabs[0].id, {imageURL});
});
}
function reportError(error) {
console.error(`Could not inject content script: ${error}`);
}
}
// Ignore the drag enter event - not used in this extension
function dragenter(e) {
e.stopPropagation();
e.preventDefault();
}
// Ignore the drag over event - not used in this extension
function dragover(e) {
e.stopPropagation();
e.preventDefault();
}

View File

@@ -0,0 +1,15 @@
html, body {
width: 100%;
height: 100%;
}
#drop_zone {
border: 5px solid blue;
width: 100%;
height: 100%;
}
#drop_zone_label {
margin: 1em;
display: block;
}

View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="sidebar.css"/>
</head>
<body>
<div id="picker_zone" style="margin:20px ;color:blue">
<input type="file" accept="image/*" id="input">
</div>
<div id="drop_zone">
<strong id="drop_zone_label">Drag an image file into this Drop Zone ...</strong>
</div>
<script src="choose_file.js"></script>
</body>
</html>

8
imagify/viewer.css Normal file
View File

@@ -0,0 +1,8 @@
html, body {
margin: 0;
padding: 0;
}
img {
width: 100%;
}

17
imagify/viewer.html Normal file
View File

@@ -0,0 +1,17 @@
<!DOCTYPE>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="viewer.css"/>
</head>
<body>
<img src="about:blank">
<script src="viewer.js"></script>
</body>
</html>

3
imagify/viewer.js Normal file
View File

@@ -0,0 +1,3 @@
const params = new URLSearchParams(window.location.search);
const imageBlobURL = params.get("blobURL");
document.querySelector("img").setAttribute("src", imageBlobURL);

View File

@@ -1,6 +1,6 @@
function showCookiesForTab(tabs) {
//get the first tab object in the array
tab = tabs.pop();
let tab = tabs.pop();
//get all cookies in the domain
var gettingAllCookies = browser.cookies.getAll({url: tab.url});
@@ -14,22 +14,22 @@ function showCookiesForTab(tabs) {
if (cookies.length > 0) {
//add an <li> item with the name and value of the cookie to the list
for (cookie of cookies) {
var li = document.createElement("li");
var content = document.createTextNode(cookie.name + ": "+ cookie.value);
for (let cookie of cookies) {
let li = document.createElement("li");
let content = document.createTextNode(cookie.name + ": "+ cookie.value);
li.appendChild(content);
cookieList.appendChild(li);
}
} else {
var p = document.createElement("p");
var content = document.createTextNode("No cookies in this tab.");
var parent = cookieList.parentNode;
let p = document.createElement("p");
let content = document.createTextNode("No cookies in this tab.");
let parent = cookieList.parentNode;
p.appendChild(content);
parent.appendChild(p);
}
});
};
}
//get active tab to run an callback function.
//it sends to our callback an array of tab objects

View File

@@ -1,9 +1,11 @@
# context-menu-demo
# menu-demo
A demo of the [contextMenus API](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/contextMenus/).
A demo of the [menus API](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/menus/).
**This add-on injects JavaScript into web pages. The `addons.mozilla.org` domain disallows this operation, so this add-on will not work properly when it's run on pages in the `addons.mozilla.org` domain.**
**This add-on uses the `menus` namespace to access the functions it needs to create menu items. Note that Chrome, Edge, and Opera all use the `contextMenus` namespace for this, so this extension will not work in these browsers. For compatibility with these browsers, Firefox also offers the `contextMenus` namespace, so to make this extension work with other browsers, use `contextMenus`.**
## What it does
This add-on adds several items to the browser's context menu:
@@ -20,9 +22,11 @@ like about:debugging.
item is clicked.
* one item that uses the "commands" property to open the add-on's sidebar.
It also adds one item to the browser's "Tools" menu.
## What it shows
* How to create various types of context menu item:
* How to create various types of menu item:
* normal
* radio
* separator

View File

@@ -1,47 +1,52 @@
{
"extensionName": {
"message": "Context menu demo",
"message": "Menu demo",
"description": "Name of the extension."
},
"extensionDescription": {
"message": "Demonstrates the contextMenus API.",
"message": "Demonstrates the menus API.",
"description": "Description of the add-on."
},
"contextMenuItemSelectionLogger": {
"menuItemSelectionLogger": {
"message": "Log '%s' to the browser console",
"description": "Title of context menu item that logs the selected text when clicked."
},
"contextMenuItemRemoveMe": {
"menuItemRemoveMe": {
"message": "Remove me!",
"description": "Title of context menu item that removes itself when clicked."
},
"contextMenuItemGreenify": {
"menuItemGreenify": {
"message": "Greenify",
"description": "Title of context menu item that adds a green border when clicked."
},
"contextMenuItemBluify": {
"menuItemBluify": {
"message": "Bluify",
"description": "Title of context menu item that adds a green border when clicked."
},
"contextMenuItemCheckMe": {
"menuItemCheckMe": {
"message": "Check me",
"description": "Title of context menu item when the item is checked."
},
"contextMenuItemUncheckMe": {
"menuItemUncheckMe": {
"message": "Uncheck me",
"description": "Title of context menu item when the item is unchecked."
},
"contextMenuItemOpenSidebar": {
"menuItemOpenSidebar": {
"message": "Open sidebar",
"description": "Title of context menu item that opens a sidebar."
},
"menuItemToolsMenu": {
"message": "Click me!",
"description": "Title of tools menu item."
}
}

View File

@@ -2,7 +2,7 @@
Called when the item has been created, or when creation failed due to an error.
We'll just log success/failure here.
*/
function onCreated(n) {
function onCreated() {
if (browser.runtime.lastError) {
console.log(`Error: ${browser.runtime.lastError}`);
} else {
@@ -29,41 +29,41 @@ function onError(error) {
/*
Create all the context menu items.
*/
browser.contextMenus.create({
browser.menus.create({
id: "log-selection",
title: browser.i18n.getMessage("contextMenuItemSelectionLogger"),
title: browser.i18n.getMessage("menuItemSelectionLogger"),
contexts: ["selection"]
}, onCreated);
browser.contextMenus.create({
browser.menus.create({
id: "remove-me",
title: browser.i18n.getMessage("contextMenuItemRemoveMe"),
title: browser.i18n.getMessage("menuItemRemoveMe"),
contexts: ["all"]
}, onCreated);
browser.contextMenus.create({
browser.menus.create({
id: "separator-1",
type: "separator",
contexts: ["all"]
}, onCreated);
browser.contextMenus.create({
browser.menus.create({
id: "greenify",
type: "radio",
title: browser.i18n.getMessage("contextMenuItemGreenify"),
title: browser.i18n.getMessage("menuItemGreenify"),
contexts: ["all"],
checked: true
}, onCreated);
browser.contextMenus.create({
browser.menus.create({
id: "bluify",
type: "radio",
title: browser.i18n.getMessage("contextMenuItemBluify"),
title: browser.i18n.getMessage("menuItemBluify"),
contexts: ["all"],
checked: false
}, onCreated);
browser.contextMenus.create({
browser.menus.create({
id: "separator-2",
type: "separator",
contexts: ["all"]
@@ -71,22 +71,27 @@ browser.contextMenus.create({
var checkedState = true;
browser.contextMenus.create({
browser.menus.create({
id: "check-uncheck",
type: "checkbox",
title: browser.i18n.getMessage("contextMenuItemUncheckMe"),
title: browser.i18n.getMessage("menuItemUncheckMe"),
contexts: ["all"],
checked: checkedState
}, onCreated);
browser.contextMenus.create({
browser.menus.create({
id: "open-sidebar",
title: browser.i18n.getMessage("contextMenuItemOpenSidebar"),
title: browser.i18n.getMessage("menuItemOpenSidebar"),
contexts: ["all"],
command: "_execute_sidebar_action"
}, onCreated);
browser.menus.create({
id: "tools-menu",
title: browser.i18n.getMessage("menuItemToolsMenu"),
contexts: ["tools_menu"],
}, onCreated);
/*
Set a colored border on the document in the given tab.
@@ -113,12 +118,12 @@ property into the event listener.
function updateCheckUncheck() {
checkedState = !checkedState;
if (checkedState) {
browser.contextMenus.update("check-uncheck", {
title: browser.i18n.getMessage("contextMenuItemUncheckMe"),
browser.menus.update("check-uncheck", {
title: browser.i18n.getMessage("menuItemUncheckMe"),
});
} else {
browser.contextMenus.update("check-uncheck", {
title: browser.i18n.getMessage("contextMenuItemCheckMe"),
browser.menus.update("check-uncheck", {
title: browser.i18n.getMessage("menuItemCheckMe"),
});
}
}
@@ -127,13 +132,13 @@ function updateCheckUncheck() {
The click event listener, where we perform the appropriate action given the
ID of the menu item that was clicked.
*/
browser.contextMenus.onClicked.addListener(function(info, tab) {
browser.menus.onClicked.addListener((info, tab) => {
switch (info.menuItemId) {
case "log-selection":
console.log(info.selectionText);
break;
case "remove-me":
var removing = browser.contextMenus.remove(info.menuItemId);
var removing = browser.menus.remove(info.menuItemId);
removing.then(onRemoved, onError);
break;
case "bluify":
@@ -148,5 +153,8 @@ browser.contextMenus.onClicked.addListener(function(info, tab) {
case "open-sidebar":
console.log("Opening my sidebar");
break;
case "tools-menu":
console.log("Clicked the tools menu item");
break;
}
});

View File

Before

Width:  |  Height:  |  Size: 251 B

After

Width:  |  Height:  |  Size: 251 B

View File

Before

Width:  |  Height:  |  Size: 344 B

After

Width:  |  Height:  |  Size: 344 B

View File

Before

Width:  |  Height:  |  Size: 310 B

After

Width:  |  Height:  |  Size: 310 B

View File

@@ -7,7 +7,7 @@
"default_locale": "en",
"applications": {
"gecko": {
"strict_min_version": "55.0a1"
"strict_min_version": "56.0a1"
}
},
@@ -16,7 +16,7 @@
},
"permissions": [
"contextMenus",
"menus",
"activeTab"
],

View File

@@ -0,0 +1,8 @@
{
"env": {
"browser": true,
"es6": true,
"amd": true,
"webextensions": true
}
}

View File

@@ -12,4 +12,4 @@ var Background = {
}
};
chrome.runtime.onMessage.addListener(Background.receiveMessage);
browser.runtime.onMessage.addListener(Background.receiveMessage);

View File

@@ -3,7 +3,7 @@
if($game.innerText !== 'ping'){
$game.innerText = 'ping';
} else{
chrome.runtime.sendMessage({action: 'ping'},function(response) {
browser.runtime.sendMessage({action: 'ping'}).then((response) => {
$game.innerText = response;
});
}

32
package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "webextensions-examples",
"title": "WebExtensions Examples",
"version": "1.0.0",
"description": "Example Firefox add-ons created using the WebExtensions API",
"devDependencies": {
"eslint": "^4.4.1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/mdn/webextensions-examples.git"
},
"scripts": {
"test": "eslint .",
"lint": "eslint ."
},
"license": "MIT",
"bugs": {
"url": "https://github.com/mdn/webextensions-examples/issues"
},
"keywords": [
"webextensions",
"webextensions-apis",
"mdn",
"firefox",
"mozilla"
],
"homepage": "https://developer.mozilla.org/Add-ons/WebExtensions/Examples",
"dependencies": {
"babel-eslint": "^7.2.3"
}
}

View File

@@ -2,7 +2,7 @@
Listen for messages from the page.
If the message was from the page script, show an alert.
*/
window.addEventListener("message", function(event) {
window.addEventListener("message", (event) => {
if (event.source == window &&
event.data &&
event.data.direction == "from-page-script") {

View File

@@ -0,0 +1,5 @@
{
"parserOptions": {
"ecmaVersion": 8
}
}

View File

@@ -7,7 +7,7 @@ const defaultSettings = {
}
// Register the proxy script
browser.proxy.registerProxyScript(proxyScriptURL);
browser.proxy.register(proxyScriptURL);
// Log any errors from the proxy script
browser.proxy.onProxyError.addListener(error => {
@@ -15,7 +15,7 @@ browser.proxy.onProxyError.addListener(error => {
});
// Initialize the proxy
function handleInit(message) {
function handleInit() {
// update the proxy whenever stored settings change
browser.storage.onChanged.addListener((newSettings) => {
browser.runtime.sendMessage(newSettings.blockedHosts.newValue, {toProxyScript: true});

View File

@@ -12,7 +12,7 @@
"applications": {
"gecko": {
"strict_min_version": "55.0a1"
"strict_min_version": "56.0a1"
}
},

View File

@@ -1,5 +1,7 @@
/* exported FindProxyForURL */
var blockedHosts = [];
const allow = "DIRECT 1234";
const allow = "DIRECT";
const deny = "PROXY 127.0.0.1:65535";
// tell the background script that we are ready

View File

@@ -27,7 +27,7 @@ function initialize() {
var gettingAllStorageItems = browser.storage.local.get(null);
gettingAllStorageItems.then((results) => {
var noteKeys = Object.keys(results);
for(noteKey of noteKeys) {
for (let noteKey of noteKeys) {
var curValue = results[noteKey];
displayNote(noteKey,curValue);
}
@@ -88,8 +88,8 @@ function displayNote(title, body) {
/* set up listener for the delete functionality */
deleteBtn.addEventListener('click',function(e){
evtTgt = e.target;
deleteBtn.addEventListener('click',(e) => {
const evtTgt = e.target;
evtTgt.parentNode.parentNode.parentNode.removeChild(evtTgt.parentNode.parentNode);
browser.storage.local.remove(title);
})
@@ -125,24 +125,24 @@ function displayNote(title, body) {
/* set up listeners for the update functionality */
noteH.addEventListener('click',function(){
noteH.addEventListener('click',() => {
noteDisplay.style.display = 'none';
noteEdit.style.display = 'block';
})
notePara.addEventListener('click',function(){
notePara.addEventListener('click',() => {
noteDisplay.style.display = 'none';
noteEdit.style.display = 'block';
})
cancelBtn.addEventListener('click',function(){
cancelBtn.addEventListener('click',() => {
noteDisplay.style.display = 'block';
noteEdit.style.display = 'none';
noteTitleEdit.value = title;
noteBodyEdit.value = body;
})
updateBtn.addEventListener('click',function(){
updateBtn.addEventListener('click',() => {
if(noteTitleEdit.value !== title || noteBodyEdit.value !== body) {
updateNote(title,noteTitleEdit.value,noteBodyEdit.value);
note.parentNode.removeChild(note);

View File

@@ -0,0 +1,15 @@
{
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"env": {
"browser": true,
"es6": true,
"amd": true,
"webextensions": true
}
}

View File

@@ -11,7 +11,7 @@ class Popup extends React.Component {
componentDidMount() {
// Get the active tab and store it in component state.
chrome.tabs.query({active: true}, tabs => {
browser.tabs.query({active: true}).then(tabs => {
this.setState({activeTab: tabs[0]});
});
}

View File

@@ -1,7 +1,7 @@
/*
copy the selected text to clipboard
*/
function copySelection(e) {
function copySelection() {
var selectedText = window.getSelection().toString().trim();
if (selectedText) {

View File

@@ -0,0 +1,36 @@
# "Image Reference Collector" example
## What it does
This example adds a context menu which targets any image element in the webpage.
When the context menu item is clicked, the add-on opens a window and
adds the related image element to the preview list of the collected images.
The user can then store the collected images by giving the collection a name
and pressing the **save** button.
Once a collection of reference images has been stored by the add-on, they
can be navigated using the extension page that the add-on will open in a tab
when the user press the add-on **browserAction**.
## What it shows
The main goal of this example is showing how to use the [idb-file-storage library](https://www.npmjs.com/package/idb-file-storage) to store and manipulate files in a WebExtension.
* How to store blob into the add-on IndexedDB storage
* How to list the stored blobs (optionally by filtering the listed blobs)
* How to turn the stored blobs into blob urls to show them in the extension page
* How to remove the stored blobs from the extension IndexedDB storage.
[![entension demo screencast](screenshots/screenshot.png "extension demo screencast")](https://youtu.be/t6aVqMMe2Rc)
This example is written in two forms:
- a plain webextension (which doesn't need any build step)
- a webextension built using webpack
The code that stores and retrieves the files from the IndexedDB storage is in the
file named `utils/image-store.js` in both the example version.
## Icons
The icon for this add-on is provided by [icons8](https://icons8.com/).

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

@@ -0,0 +1 @@
deps

View File

@@ -0,0 +1,3 @@
{
"parser": "babel-eslint"
}

View File

@@ -0,0 +1,39 @@
# "Image Reference Collector" example without a webpack build step (and React UI)
## Usage
This version of the example doesn't use Webpack and Babel to transpile the ES6 modules (and JSX)
into JavaScript bundle scripts, so it can be executed (using `web-ext run` or by installing it temporarily from "about:debugging#addons") and changed without any build step.
## NOTE on the plain JavaScript React UI
The UI of this example is based on React (as is the "build with webpack" version of this example), but it uses plain JavaScript instead of JSX (the "HTML"-like syntax usually used in "React"-based projects), and so the component UI hierarchy is composed of `React.createElement` function calls, e.g.
```
class MyReactComponent extends React.Component {
render() {
return (
<div className="important">
<h3>A title</h3>
<p>A text paragraph</p>
</div>
);
}
}
```
in plain Javascript (without JSX) this becomes:
```
// Shortcut for React components render methods.
const el = React.createElement;
class Popup extends React.Component {
render() {
return el("div", {className: "important"}, [
el("h3", {}, "A title"),
el("p", {}, "A text paragraph"),
]);
}
}
```

View File

@@ -0,0 +1,47 @@
// Open the UI to navigate the collection images in a tab.
browser.browserAction.onClicked.addListener(() => {
browser.tabs.create({url: "/navigate-collection.html"});
});
// Add a context menu action on every image element in the page.
browser.contextMenus.create({
id: "collect-image",
title: "Add to the collected images",
contexts: ["image"],
});
// Manage pending collected images.
let pendingCollectedUrls = [];
browser.runtime.onMessage.addListener((msg) => {
if (msg.type === "get-pending-collected-urls") {
let urls = pendingCollectedUrls;
pendingCollectedUrls = [];
return Promise.resolve(urls);
}
});
// Handle the context menu action click events.
browser.contextMenus.onClicked.addListener(async (info) => {
try {
await browser.runtime.sendMessage({
type: "new-collected-images",
url: info.srcUrl,
});
} catch (err) {
if (err.message.includes("Could not establish connection. Receiving end does not exist.")) {
// Add the url to the pending urls and open a popup.
pendingCollectedUrls.push(info.srcUrl);
try {
await browser.windows.create({
type: "popup", url: "/popup.html",
top: 0, left: 0, width: 300, height: 400,
});
} catch (err) {
console.error(err);
}
return;
}
console.error(err);
}
});

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
!function(n){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=n();else if("function"==typeof define&&define.amd)define([],n);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.uuidv4=n()}}(function(){return function n(e,r,o){function t(f,u){if(!r[f]){if(!e[f]){var a="function"==typeof require&&require;if(!u&&a)return a(f,!0);if(i)return i(f,!0);var d=new Error("Cannot find module '"+f+"'");throw d.code="MODULE_NOT_FOUND",d}var l=r[f]={exports:{}};e[f][0].call(l.exports,function(n){var r=e[f][1][n];return t(r?r:n)},l,l.exports,n,e,r,o)}return r[f].exports}for(var i="function"==typeof require&&require,f=0;f<o.length;f++)t(o[f]);return t}({1:[function(n,e,r){function o(n,e){var r=e||0,o=t;return o[n[r++]]+o[n[r++]]+o[n[r++]]+o[n[r++]]+"-"+o[n[r++]]+o[n[r++]]+"-"+o[n[r++]]+o[n[r++]]+"-"+o[n[r++]]+o[n[r++]]+"-"+o[n[r++]]+o[n[r++]]+o[n[r++]]+o[n[r++]]+o[n[r++]]+o[n[r++]]}for(var t=[],i=0;i<256;++i)t[i]=(i+256).toString(16).substr(1);e.exports=o},{}],2:[function(n,e,r){(function(n){var r,o=n.crypto||n.msCrypto;if(o&&o.getRandomValues){var t=new Uint8Array(16);r=function(){return o.getRandomValues(t),t}}if(!r){var i=new Array(16);r=function(){for(var n,e=0;e<16;e++)0===(3&e)&&(n=4294967296*Math.random()),i[e]=n>>>((3&e)<<3)&255;return i}}e.exports=r}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],3:[function(n,e,r){function o(n,e,r){var o=e&&r||0;"string"==typeof n&&(e="binary"==n?new Array(16):null,n=null),n=n||{};var f=n.random||(n.rng||t)();if(f[6]=15&f[6]|64,f[8]=63&f[8]|128,e)for(var u=0;u<16;++u)e[o+u]=f[u];return e||i(f)}var t=n("./lib/rng"),i=n("./lib/bytesToUuid");e.exports=o},{"./lib/bytesToUuid":1,"./lib/rng":2}]},{},[3])(3)});

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1008 B

View File

@@ -0,0 +1,26 @@
{
"manifest_version": 2,
"name": "store-collected-images",
"version": "1.0",
"icons": {
"16": "images/icon16.png",
"48": "images/icon.png"
},
"browser_action": {
"default_icon": {
"48": "images/icon.png"
},
"default_title": "Collected Images"
},
"background": {
"scripts": ["background.js"]
},
"permissions": [
"contextMenus",
"<all_urls>"
]
}

View File

@@ -0,0 +1 @@
@import "shared.css";

View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="navigate-collection.css"/>
</head>
<body>
<div id="app">
<h3>Stored images</h3>
<input placeholder="filter image" class="image-filter">
<button class="reload-images">reload</button>
<button class="delete-images">delete</button>
<ul class="thumbnails"></ul>
</div>
<script src="/deps/idb-file-storage.js"></script>
<script src="/deps/uuidv4.js"></script>
<script src="/utils/handle-window-drag-and-drop.js"></script>
<script src="/utils/image-store.js"></script>
<script src="navigate-collection.js"></script>
</body>
</html>

View File

@@ -0,0 +1,85 @@
/* global loadStoredImages, removeStoredImages */
"use strict";
class NavigateCollectionUI {
constructor(containerEl) {
this.containerEl = containerEl;
this.state = {
storedImages: [],
};
this.onFilterUpdated = this.onFilterUpdated.bind(this);
this.onReload = this.onFilterUpdated;
this.onDelete = this.onDelete.bind(this);
this.containerEl.querySelector("button.reload-images").onclick = this.onReload;
this.containerEl.querySelector("button.delete-images").onclick = this.onDelete;
this.containerEl.querySelector("input.image-filter").onchange = this.onFilterUpdated;
// Load the stored image once the component has been rendered in the page.
this.onFilterUpdated();
}
get imageFilterValue() {
return this.containerEl.querySelector("input.image-filter").value;
}
set imageFilterValue(value) {
return this.containerEl.querySelector("input.image-filter").value = value;
}
setState(state) {
// Merge the new state on top of the previous one and re-render everything.
this.state = Object.assign(this.state, state);
this.render();
}
componentDidMount() {
// Load the stored image once the component has been rendered in the page.
this.onFilterUpdated();
}
onFilterUpdated() {
loadStoredImages(this.imageFilterValue)
.then((storedImages) => {
this.setState({storedImages});
})
.catch(console.error);
}
onDelete() {
const {storedImages} = this.state;
this.setState({storedImages: []});
removeStoredImages(storedImages).catch(console.error);
}
render() {
const {storedImages} = this.state;
const thumbnailsUl = this.containerEl.querySelector("ul.thumbnails");
while (thumbnailsUl.firstChild) {
thumbnailsUl.removeChild(thumbnailsUl.firstChild);
}
storedImages.forEach(({storedName, blobUrl}) => {
const onClickedImage = () => {
this.imageFilterValue = storedName;
this.onFilterUpdated();
};
const li = document.createElement("li");
const img = document.createElement("img");
li.setAttribute("id", storedName);
img.setAttribute("src", blobUrl);
img.onclick = onClickedImage;
li.appendChild(img);
thumbnailsUl.appendChild(li);
});
}
}
// eslint-disable-next-line no-unused-vars
const navigateCollectionUI = new NavigateCollectionUI(document.getElementById('app'));

View File

@@ -0,0 +1,12 @@
@import "shared.css";
html, body {
width: 250px;
margin: 0;
padding: 0;
margin-left: 1em;
}
input {
width: 90%;
}

View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="popup.css"/>
</head>
<body>
<div id="app">
<h3>Collected images</h3>
<p id="error-message"></p>
<input placeholder="collection name" class="collection-name">
<button class="save-collection">save</button>
<ul class="thumbnails">
<ul>
</div>
<script src="/deps/idb-file-storage.js"></script>
<script src="/deps/uuidv4.js"></script>
<script src="/utils/handle-window-drag-and-drop.js"></script>
<script src="/utils/image-store.js"></script>
<script src="popup.js"></script>
</body>
</html>

View File

@@ -0,0 +1,127 @@
/* global saveCollectedBlobs, uuidv4, preventWindowDragAndDrop */
"use strict";
class Popup {
constructor(containerEl) {
this.containerEl = containerEl;
this.state = {
collectedBlobs: [],
lastMessage: undefined,
};
this.onClick = this.onClick.bind(this);
this.containerEl.querySelector("button.save-collection").onclick = this.onClick;
}
get collectionNameValue() {
return this.containerEl.querySelector("input.collection-name").value;
}
setState(state) {
// Merge the new state on top of the previous one and re-render everything.
this.state = Object.assign(this.state, state);
this.render();
}
onClick() {
if (!this.collectionNameValue) {
this.setState({
lastMessage: {text: "The collection name is mandatory.", type: "error"},
});
setTimeout(() => {
this.setState({lastMessage: undefined});
}, 2000);
return;
}
saveCollectedBlobs(this.collectionNameValue, this.state.collectedBlobs)
.then(() => {
this.setState({
lastMessage: {text: "All the collected images have been saved", type: "success"},
collectedBlobs: [],
});
setTimeout(() => {
this.setState({lastMessage: undefined});
}, 2000);
})
.catch((err) => {
this.setState({
lastMessage: {text: `Failed to save collected images: ${err}`, type: "error"},
});
setTimeout(() => {
this.setState({lastMessage: undefined});
}, 2000);
});
}
render() {
const {collectedBlobs, lastMessage} = this.state;
const lastMessageEl = this.containerEl.querySelector("p#error-message");
if (lastMessage) {
lastMessageEl.setAttribute("class", lastMessage.type);
lastMessageEl.textContent = lastMessage.text;
} else {
lastMessageEl.setAttribute("class", "");
lastMessageEl.textContent = "";
}
const thumbnailsUl = this.containerEl.querySelector("ul.thumbnails");
while (thumbnailsUl.firstChild) {
thumbnailsUl.removeChild(thumbnailsUl.firstChild);
}
collectedBlobs.forEach(({uuid, blobUrl}) => {
const li = document.createElement("li");
const img = document.createElement("img");
li.setAttribute("id", uuid);
img.setAttribute("src", blobUrl);
li.appendChild(img);
thumbnailsUl.appendChild(li);
});
}
}
const popup = new Popup(document.getElementById('app'));
async function fetchBlobFromUrl(fetchUrl) {
const res = await fetch(fetchUrl);
const blob = await res.blob();
return {
blob,
blobUrl: URL.createObjectURL(blob),
fetchUrl,
uuid: uuidv4(),
};
}
preventWindowDragAndDrop();
browser.runtime.onMessage.addListener(async (msg) => {
if (msg.type === "new-collected-images") {
let collectedBlobs = popup.state.collectedBlobs || [];
const fetchRes = await fetchBlobFromUrl(msg.url);
collectedBlobs.push(fetchRes);
popup.setState({collectedBlobs});
return true;
}
});
browser.runtime.sendMessage({type: "get-pending-collected-urls"}).then(async res => {
let collectedBlobs = popup.state.collectedBlobs || [];
for (const url of res) {
const fetchRes = await fetchBlobFromUrl(url);
collectedBlobs.push(fetchRes);
popup.setState({collectedBlobs});
}
});

View File

@@ -0,0 +1,22 @@
ul.thumbnails {
padding: 0;
}
ul.thumbnails li {
display: inline-block;
vertical-align: middle;
padding: 0.4em;
}
.thumbnails img {
max-width: 50px;
max-height: 50px;
}
.error {
background: rgba(255,0,0,0.4);
}
.success {
background: rgba(0,255,0,0.4);
}

View File

@@ -0,0 +1,14 @@
/* exported preventWindowDragAndDrop */
"use strict";
function preventWindowDragAndDrop() {
function preventDefault(ev) {
ev.preventDefault();
return true;
}
window.ondragover = preventDefault;
window.ondragend = preventDefault;
window.ondrop = preventDefault;
}

View File

@@ -0,0 +1,37 @@
/* global IDBFiles */
/* exported saveCollectedBlobs, loadStoredImages, removeStoredImages */
"use strict";
async function saveCollectedBlobs(collectionName, collectedBlobs) {
const storedImages = await IDBFiles.getFileStorage({name: "stored-images"});
for (const item of collectedBlobs) {
await storedImages.put(`${collectionName}/${item.uuid}`, item.blob);
}
}
async function loadStoredImages(filter) {
const imagesStore = await IDBFiles.getFileStorage({name: "stored-images"});
let listOptions = filter ? {includes: filter} : undefined;
const imagesList = await imagesStore.list(listOptions);
let storedImages = [];
for (const storedName of imagesList) {
const blob = await imagesStore.get(storedName);
storedImages.push({storedName, blobUrl: URL.createObjectURL(blob)});
}
return storedImages;
}
async function removeStoredImages(storedImages) {
const imagesStore = await IDBFiles.getFileStorage({name: "stored-images"});
for (const storedImage of storedImages) {
URL.revokeObjectURL(storedImage.blobUrl);
await imagesStore.remove(storedImage.storedName);
}
}

View File

@@ -0,0 +1,6 @@
{
"parser": "babel-eslint",
"env": {
"commonjs": true
}
}

View File

@@ -0,0 +1,5 @@
# Ignore build artifacts and other files.
.DS_Store
yarn.lock
extension/dist
node_modules

View File

@@ -0,0 +1,32 @@
# "Image Reference Collector" example built with webpack (and React UI)
## Usage
This example is built using Babel and Webpack, and so the transpiled bundles have to
be built first:
you need to change into the example subdirectory and install all
[NodeJS][nodejs] dependencies with [npm](http://npmjs.com/) or
[yarn](https://yarnpkg.com/):
npm install
You can build the extension using:
npm run build
This creates the source bundles for the WebExtension in the `extension` subdirectory, and
you can manually install the add-on on Firefox by loading the `extension` from the
"about:debugging#addons" page.
You can also build the sources and start a new Firefox instance with the add-on installed
in one command:
npm run start
To start a webpack instance that automatically rebuilds the add-on when
you change the sources, in another shell window, you can run the following npm script:
npm run build:watch
While this npm script is running, any time you edit a file, it will be rebuilt automatically.

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1008 B

View File

@@ -0,0 +1,26 @@
{
"manifest_version": 2,
"name": "store-collected-images",
"version": "1.0",
"icons": {
"16": "images/icon16.png",
"48": "images/icon.png"
},
"browser_action": {
"default_icon": {
"48": "images/icon.png"
},
"default_title": "Collected Images"
},
"background": {
"scripts": ["dist/background.js"]
},
"permissions": [
"contextMenus",
"<all_urls>"
]
}

View File

@@ -0,0 +1 @@
@import "shared.css";

View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="navigate-collection.css"/>
</head>
<body>
<div id="app"></div>
<script src="dist/navigate-collection.js"></script>
</body>
</html>

View File

@@ -0,0 +1,12 @@
@import "shared.css";
html, body {
width: 250px;
margin: 0;
padding: 0;
margin-left: 1em;
}
input {
width: 90%;
}

View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="popup.css"/>
</head>
<body>
<div id="app"></div>
<script src="dist/popup.js"></script>
</body>
</html>

View File

@@ -0,0 +1,22 @@
ul.thumbnails {
padding: 0;
}
ul.thumbnails li {
display: inline-block;
vertical-align: middle;
padding: 0.4em;
}
.thumbnails img {
max-width: 50px;
max-height: 50px;
}
.error {
background: rgba(255,0,0,0.4);
}
.success {
background: rgba(0,255,0,0.4);
}

View File

@@ -0,0 +1,38 @@
{
"name": "store-collected-images",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack --display-error-details --progress --colors",
"build:watch": "npm run build -- -w",
"start": "npm run build && web-ext run -s extension/"
},
"author": "",
"license": "MPL-2.0",
"devDependencies": {
"babel-core": "6.24.1",
"babel-loader": "7.0.0",
"babel-plugin-transform-class-properties": "6.24.1",
"babel-plugin-transform-object-rest-spread": "6.23.0",
"babel-plugin-transform-es2015-modules-commonjs": "6.24.1",
"babel-preset-es2017": "6.24.1",
"babel-preset-react": "6.24.1",
"idb-file-storage": "^0.1.0",
"react": "15.5.4",
"react-dom": "15.5.4",
"uuid": "^3.0.1",
"web-ext": "1.9.1",
"webpack": "2.6.1"
},
"babel": {
"presets": [
"es2017",
"react"
],
"plugins": [
"transform-class-properties",
"transform-es2015-modules-commonjs"
]
}
}

Some files were not shown because too many files have changed in this diff Show More