From d5183199f7291fad69ca91c13498fe5f2400e9d5 Mon Sep 17 00:00:00 2001 From: Sean Zhu Date: Tue, 30 Apr 2019 23:43:10 -0700 Subject: [PATCH] Break out into imports --- extension/js/background.js | 12 +- extension/js/base.js | 240 ------------------------------ extension/js/lib/ChromeApiUtil.js | 60 ++++++++ extension/js/lib/DomUtil.js | 16 ++ extension/js/lib/IterUtil.js | 32 ++++ extension/js/lib/OptionsUtil.js | 33 ++++ extension/js/lib/PromiseUtil.js | 17 +++ extension/js/lib/TimerUtil.js | 69 +++++++++ extension/js/lib/Urls.js | 40 +++++ extension/js/options.js | 41 +---- extension/page-background.html | 1 - extension/page-options.html | 1 - 12 files changed, 278 insertions(+), 284 deletions(-) delete mode 100644 extension/js/base.js create mode 100644 extension/js/lib/ChromeApiUtil.js create mode 100644 extension/js/lib/DomUtil.js create mode 100644 extension/js/lib/IterUtil.js create mode 100644 extension/js/lib/OptionsUtil.js create mode 100644 extension/js/lib/PromiseUtil.js create mode 100644 extension/js/lib/TimerUtil.js create mode 100644 extension/js/lib/Urls.js diff --git a/extension/js/background.js b/extension/js/background.js index c71e1ec..792d004 100644 --- a/extension/js/background.js +++ b/extension/js/background.js @@ -1,10 +1,8 @@ -import { - ChromeApiUtil, - IterUtil, - PromiseUtil, - TimerUtil, - Urls, -} from './base.js'; +import * as ChromeApiUtil from './lib/ChromeApiUtil.js'; +import * as IterUtil from './lib/IterUtil.js'; +import * as PromiseUtil from './lib/PromiseUtil.js'; +import * as TimerUtil from './lib/TimerUtil.js'; +import * as Urls from './lib/Urls.js'; let Chrome = ChromeApiUtil.getPromiseVersions([ 'chrome.storage.sync.get', diff --git a/extension/js/base.js b/extension/js/base.js deleted file mode 100644 index 850e278..0000000 --- a/extension/js/base.js +++ /dev/null @@ -1,240 +0,0 @@ -export const IterUtil = { - // Return the item in items with the largest getKeyByItem(item). - max(items, getKeyByItem) { - return items.reduce((item1, item2) => { - return getKeyByItem(item1) > getKeyByItem(item2) ? item1 : item2; - }); - }, - - // Zip that works with iterables. - // From http://stackoverflow.com/a/10284006/782045 - zip(rows) { - return rows[0].map((_, c) => { - return rows.map((row) => row[c]); - }); - }, - - // Like zip, but with objects. - // Example: - // - input: {num: [1, 2, 3], str: ["a", "b", "c"]} - // - output: [{num: 1, str: "a"}, {num: 2, str: "b"}, {num: 3, str: "c"}] - mapzip(unzipped) { - let zipped = []; - for (let key of Object.keys(unzipped)) { - let vals = unzipped[key]; - for (let i = 0; i < vals.length; i++) { - zipped[i] = zipped[i] || {}; - zipped[i][key] = vals[i]; - } - } - return zipped; - }, -}; - -export const PromiseUtil = { - // Like Promise.all, but with objects. - map(promiseByKey) { - let keys = Object.keys(promiseByKey); - return Promise.resolve() - .then(() => { - return Promise.all(keys.map((key) => promiseByKey[key])); - }) - .then((results) => { - let resultsByKey = {}; - for (let i = 0; i < keys.length; i++) { - resultsByKey[keys[i]] = results[i]; - } - return resultsByKey; - }); - }, -}; - -export const TimerUtil = { - // A promise-returning version of setTimeout. - setTimeout(timeout) { - let timerId; - let rejectFunction; - let promise = new Promise((resolve, reject) => { - timerId = setTimeout(resolve, timeout); - rejectFunction = reject; - }); - promise.cancel = () => { - clearTimeout(timerId); - rejectFunction(); - }; - return promise; - }, - - // A promise-returning use case of setInterval. - pollUntil(interval, stopCondition) { - let intervalId; - let rejectFunction; - let promise = new Promise((resolve, reject) => { - intervalId = setInterval(() => { - if (stopCondition()) { - clearInterval(intervalId); - resolve(); - } - }, interval); - rejectFunction = reject; - }); - promise.cancel = () => { - window.cancelInterval(intervalId); - rejectFunction(); - }; - return promise; - }, - - // Detect double-clicking-like actions. - DoubleAction: class DoubleAction { - constructor({ timeout, onSingle, onDouble }) { - if (!(this instanceof TimerUtil.DoubleAction)) { - throw new Error('TimerUtil.DoubleAction must be initialized using new'); - } - this.timeout = timeout; - this.onSingle = onSingle; - this.onDouble = onDouble; - this.timeoutId = undefined; - } - - _dispatch = (callback) => { - clearTimeout(this.timeoutId); - this.timeoutId = undefined; - callback(); - }; - - trigger = () => { - // console.log("Click!"); - if (this.timeoutId != null) { - // console.log("Double!"); - this._dispatch(this.onDouble); - } else { - // console.log("Starting timer..."); - this.timeoutId = setTimeout(() => { - // console.log("Single!"); - this._dispatch(this.onSingle); - }, this.timeout); - } - }; - }, -}; - -// https://gist.github.com/josh/8177583 -export const DOMUtil = { - ready: new Promise((resolve) => { - if (document.readyState === 'complete') { - resolve(); - } else { - let onReady = () => { - resolve(); - document.removeEventListener('DOMContentLoaded', onReady, true); - window.removeEventListener('load', onReady, true); - }; - document.addEventListener('DOMContentLoaded', onReady, true); - window.addEventListener('load', onReady, true); - } - }), -}; - -export const ChromeApiUtil = { - // Make a promise-returning versions of the chrome API method. - makePromiseVersion(theThis, theMethod, theMethodName) { - return (...theArguments) => { - return new Promise((resolve, reject) => { - theMethod.apply( - theThis, - theArguments.concat((result) => { - return chrome.runtime.lastError == null - ? resolve(result) - : reject( - new Error({ - function: theMethodName, - arguments: theArguments, - error: chrome.runtime.lastError, - }), - ); - }), - ); - }); - }; - }, - - // Make a promise-returning versions of the chrome API method, - // given a string like "chrome.windows.create". - assignPromiseVersionByMethodName(dstRoot, fullMethodName) { - // Example: let fullMethodName = 'chrome.some.thing.tabs.create' - let methodParentParts = fullMethodName.split('.'); - // Now methodParentParts == ['chrome', 'some', 'thing', 'tabs', 'create'] - - let [methodName] = methodParentParts.splice(-1); - // Now methodParentParts == ['chrome', 'some', 'thing', 'tabs'] - // Now methodName == 'create' - - let srcParent = window; - for (let methodParentPart of methodParentParts) { - srcParent = srcParent[methodParentPart]; - } - // Now srcParent == window.chrome.some.thing.tabs - // srcMethod = srcParent[methodName]; // TODO: is this line necessary? - - let dstParent = dstRoot; - for (let methodParentPart of methodParentParts.slice(1)) { - if (dstParent[methodParentPart] == null) dstParent[methodParentPart] = {}; - dstParent = dstParent[methodParentPart]; - } - // Now dstParent == dstRoot.some.thing.tabs - dstParent[methodName] = this.makePromiseVersion( - srcParent, - srcParent[methodName], - fullMethodName, - ); - }, - - getPromiseVersions(fullMethodNames) { - let dstRoot = {}; - for (let fullMethodName of fullMethodNames) { - this.assignPromiseVersionByMethodName(dstRoot, fullMethodName); - } - return dstRoot; - }, -}; - -export const Chrome = ChromeApiUtil.getPromiseVersions([ - 'chrome.storage.sync.get', - 'chrome.storage.sync.set', -]); - -export const Urls = { - stringify(urls) { - return urls.map((url) => `${url}\n`).join(''); - }, - - parse(urlsAsText) { - // Filter out blank lines - return urlsAsText.split('\n').filter((line) => line); - }, - - normalize(urlsAsText) { - return this.stringify(this.parse(urlsAsText)); - }, - - saveText(urlsAsText) { - return Promise.resolve().then(() => { - return Chrome.storage.sync.set({ - urls: this.normalize(urlsAsText), - }); - }); - }, - - load() { - return Promise.resolve() - .then(() => { - return Chrome.storage.sync.get({ - urls: '', - }); - }) - .then(({ urls }) => { - return this.parse(urls); - }); - }, -}; diff --git a/extension/js/lib/ChromeApiUtil.js b/extension/js/lib/ChromeApiUtil.js new file mode 100644 index 0000000..babaeec --- /dev/null +++ b/extension/js/lib/ChromeApiUtil.js @@ -0,0 +1,60 @@ +// @ts-check + +// Make a promise-returning versions of the chrome API method. +export function makePromiseVersion(theThis, theMethod, theMethodName) { + return (...theArguments) => { + return new Promise((resolve, reject) => { + theMethod.apply( + theThis, + theArguments.concat((result) => { + return chrome.runtime.lastError == null + ? resolve(result) + : reject({ + function: theMethodName, + arguments: theArguments, + error: chrome.runtime.lastError, + }); + }), + ); + }); + }; +} + +// Make a promise-returning versions of the chrome API method, +// given a string like "chrome.windows.create". +export function assignPromiseVersionByMethodName(dstRoot, fullMethodName) { + // Example: let fullMethodName = 'chrome.some.thing.tabs.create' + let methodParentParts = fullMethodName.split('.'); + // Now methodParentParts == ['chrome', 'some', 'thing', 'tabs', 'create'] + + let [methodName] = methodParentParts.splice(-1); + // Now methodParentParts == ['chrome', 'some', 'thing', 'tabs'] + // Now methodName == 'create' + + let srcParent = window; + for (let methodParentPart of methodParentParts) { + srcParent = srcParent[methodParentPart]; + } + // Now srcParent == window.chrome.some.thing.tabs + // srcMethod = srcParent[methodName]; // TODO: is this line necessary? + + let dstParent = dstRoot; + for (let methodParentPart of methodParentParts.slice(1)) { + if (dstParent[methodParentPart] == null) dstParent[methodParentPart] = {}; + dstParent = dstParent[methodParentPart]; + } + // Now dstParent == dstRoot.some.thing.tabs + dstParent[methodName] = this.makePromiseVersion( + srcParent, + srcParent[methodName], + fullMethodName, + ); +} + +export function getPromiseVersions(fullMethodNames) { + let dstRoot = {}; + for (let fullMethodName of fullMethodNames) { + this.assignPromiseVersionByMethodName(dstRoot, fullMethodName); + } + return dstRoot; +} diff --git a/extension/js/lib/DomUtil.js b/extension/js/lib/DomUtil.js new file mode 100644 index 0000000..592741d --- /dev/null +++ b/extension/js/lib/DomUtil.js @@ -0,0 +1,16 @@ +// @ts-check + +// https://gist.github.com/josh/8177583 +export const Ready = new Promise((resolve) => { + if (document.readyState === 'complete') { + resolve(); + } else { + let onReady = () => { + resolve(); + document.removeEventListener('DOMContentLoaded', onReady, true); + window.removeEventListener('load', onReady, true); + }; + document.addEventListener('DOMContentLoaded', onReady, true); + window.addEventListener('load', onReady, true); + } +}); diff --git a/extension/js/lib/IterUtil.js b/extension/js/lib/IterUtil.js new file mode 100644 index 0000000..35aff5a --- /dev/null +++ b/extension/js/lib/IterUtil.js @@ -0,0 +1,32 @@ +// @ts-check + +// Return the item in items with the largest getKeyByItem(item). +export function max(items, getKeyByItem) { + return items.reduce((item1, item2) => { + return getKeyByItem(item1) > getKeyByItem(item2) ? item1 : item2; + }); +} + +// Zip that works with iterables. +// From http://stackoverflow.com/a/10284006/782045 +export function zip(rows) { + return rows[0].map((_, c) => { + return rows.map((row) => row[c]); + }); +} + +// Like zip, but with objects. +// Example: +// - input: {num: [1, 2, 3], str: ["a", "b", "c"]} +// - output: [{num: 1, str: "a"}, {num: 2, str: "b"}, {num: 3, str: "c"}] +export function mapzip(unzipped) { + let zipped = []; + for (let key of Object.keys(unzipped)) { + let vals = unzipped[key]; + for (let i = 0; i < vals.length; i++) { + zipped[i] = zipped[i] || {}; + zipped[i][key] = vals[i]; + } + } + return zipped; +} diff --git a/extension/js/lib/OptionsUtil.js b/extension/js/lib/OptionsUtil.js new file mode 100644 index 0000000..f8286e6 --- /dev/null +++ b/extension/js/lib/OptionsUtil.js @@ -0,0 +1,33 @@ +// @ts-check +import * as TimerUtil from './TimerUtil.js'; +import * as Urls from './Urls.js'; + +export function save() { + Promise.resolve() + .then(() => { + let urlsAsText = document.getElementById('js-input-urls').value; + return Urls.saveText(urlsAsText); + }) + .then(() => { + return this.restore(); + }) + .then(() => { + document.getElementById('js-status').textContent = 'Options saved.'; + }) + .then(() => { + return TimerUtil.setTimeout(2000); + }) + .then(() => { + document.getElementById('js-status').textContent = ''; + }); +} + +export function restore() { + return Promise.resolve() + .then(() => { + return Urls.load(); + }) + .then((urls) => { + document.getElementById('js-input-urls').value = Urls.stringify(urls); + }); +} diff --git a/extension/js/lib/PromiseUtil.js b/extension/js/lib/PromiseUtil.js new file mode 100644 index 0000000..6cad176 --- /dev/null +++ b/extension/js/lib/PromiseUtil.js @@ -0,0 +1,17 @@ +// @ts-check + +// Like Promise.all, but with objects. +export function map(promiseByKey) { + let keys = Object.keys(promiseByKey); + return Promise.resolve() + .then(() => { + return Promise.all(keys.map((key) => promiseByKey[key])); + }) + .then((results) => { + let resultsByKey = {}; + for (let i = 0; i < keys.length; i++) { + resultsByKey[keys[i]] = results[i]; + } + return resultsByKey; + }); +} diff --git a/extension/js/lib/TimerUtil.js b/extension/js/lib/TimerUtil.js new file mode 100644 index 0000000..3d5a5ac --- /dev/null +++ b/extension/js/lib/TimerUtil.js @@ -0,0 +1,69 @@ +// @ts-check + +// A promise-returning version of setTimeout. +export function setTimeout(timeout) { + let timerId; + let rejectFunction; + let promise = new Promise((resolve, reject) => { + timerId = window.setTimeout(resolve, timeout); + rejectFunction = reject; + }); + promise.cancel = () => { + clearTimeout(timerId); + rejectFunction(); + }; + return promise; +} + +// A promise-returning use case of setInterval. +export function pollUntil(interval, stopCondition) { + let intervalId; + let rejectFunction; + let promise = new Promise((resolve, reject) => { + intervalId = setInterval(() => { + if (stopCondition()) { + clearInterval(intervalId); + resolve(); + } + }, interval); + rejectFunction = reject; + }); + promise.cancel = () => { + window.clearInterval(intervalId); + rejectFunction(); + }; + return promise; +} + +// Detect double-clicking-like actions. +export class DoubleAction { + constructor({ timeout, onSingle, onDouble }) { + if (!(this instanceof DoubleAction)) { + throw new Error('TimerUtil.DoubleAction must be initialized using new'); + } + this.timeout = timeout; + this.onSingle = onSingle; + this.onDouble = onDouble; + this.timeoutId = undefined; + } + + _dispatch = (callback) => { + clearTimeout(this.timeoutId); + this.timeoutId = undefined; + callback(); + }; + + trigger = () => { + // console.log("Click!"); + if (this.timeoutId != null) { + // console.log("Double!"); + this._dispatch(this.onDouble); + } else { + // console.log("Starting timer..."); + this.timeoutId = window.setTimeout(() => { + // console.log("Single!"); + this._dispatch(this.onSingle); + }, this.timeout); + } + }; +} diff --git a/extension/js/lib/Urls.js b/extension/js/lib/Urls.js new file mode 100644 index 0000000..3c00f5a --- /dev/null +++ b/extension/js/lib/Urls.js @@ -0,0 +1,40 @@ +// @ts-check +import * as ChromeApiUtil from './ChromeApiUtil.js'; + +const Chrome = ChromeApiUtil.getPromiseVersions([ + 'chrome.storage.sync.get', + 'chrome.storage.sync.set', +]); + +export function stringify(urls) { + return urls.map((url) => `${url}\n`).join(''); +} + +export function parse(urlsAsText) { + // Filter out blank lines + return urlsAsText.split('\n').filter((line) => line); +} + +export function normalize(urlsAsText) { + return this.stringify(this.parse(urlsAsText)); +} + +export function saveText(urlsAsText) { + return Promise.resolve().then(() => { + return Chrome.storage.sync.set({ + urls: this.normalize(urlsAsText), + }); + }); +} + +export function load() { + return Promise.resolve() + .then(() => { + return Chrome.storage.sync.get({ + urls: '', + }); + }) + .then(({ urls }) => { + return this.parse(urls); + }); +} diff --git a/extension/js/options.js b/extension/js/options.js index 9a6468b..bddafe8 100644 --- a/extension/js/options.js +++ b/extension/js/options.js @@ -1,39 +1,10 @@ -import { DOMUtil, TimerUtil, Urls } from './base.js'; - -let Options = { - save() { - Promise.resolve() - .then(() => { - let urlsAsText = document.getElementById('js-input-urls').value; - return Urls.saveText(urlsAsText); - }) - .then(() => { - return this.restore(); - }) - .then(() => { - document.getElementById('js-status').textContent = 'Options saved.'; - }) - .then(() => { - return TimerUtil.setTimeout(2000); - }) - .then(() => { - document.getElementById('js-status').textContent = ''; - }); - }, - - restore() { - return Promise.resolve() - .then(() => { - return Urls.load(); - }) - .then((urls) => { - document.getElementById('js-input-urls').value = Urls.stringify(urls); - }); - }, -}; +// @ts-check +import * as DomUtil from './lib/DomUtil.js'; +import * as Options from './lib/OptionsUtil.js'; +import * as TimerUtil from './lib/TimerUtil.js'; Promise.resolve(() => { - return DOMUtil.ready(); + return DomUtil.Ready; }) .then(() => { return Options.restore(); @@ -45,7 +16,7 @@ Promise.resolve(() => { }); Promise.resolve(() => { - return DOMUtil.ready(); + return DomUtil.Ready; }) .then(() => { document.documentElement.style.minHeight = '1000px'; diff --git a/extension/page-background.html b/extension/page-background.html index 8da965e..d207082 100644 --- a/extension/page-background.html +++ b/extension/page-background.html @@ -2,7 +2,6 @@ Background - diff --git a/extension/page-options.html b/extension/page-options.html index fc6b402..6f67dd1 100644 --- a/extension/page-options.html +++ b/extension/page-options.html @@ -29,7 +29,6 @@ flex-grow: 1; } -