UNPKG

webext-options-sync-per-domain

Version:

Helps you manage and autosave your extension's options, separately for each additional permission.

180 lines (179 loc) 9.18 kB
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) { var useValue = arguments.length > 2; for (var i = 0; i < initializers.length; i++) { value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg); } return useValue ? value : void 0; }; var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; } var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); var _, done = false; for (var i = decorators.length - 1; i >= 0; i--) { var context = {}; for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p]; for (var p in contextIn.access) context.access[p] = contextIn.access[p]; context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); }; var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context); if (kind === "accessor") { if (result === void 0) continue; if (result === null || typeof result !== "object") throw new TypeError("Object expected"); if (_ = accept(result.get)) descriptor.get = _; if (_ = accept(result.set)) descriptor.set = _; if (_ = accept(result.init)) initializers.unshift(_); } else if (_ = accept(result)) { if (kind === "field") initializers.unshift(_); else descriptor[key] = _; } } if (target) Object.defineProperty(target, contextIn.name, descriptor); done = true; }; import { memoizeDecorator } from 'memoize'; import { patternToRegex } from 'webext-patterns'; import OptionsSync from 'webext-options-sync'; import { isBackground, isContentScript } from 'webext-detect'; import { queryAdditionalPermissions, normalizeManifestPermissions, } from 'webext-permissions'; // Export OptionsSync so that OptionsSyncPerDomain users can use it in `options-storage` without depending on it directly export * from 'webext-options-sync'; export { default as OptionsSync } from 'webext-options-sync'; function parseHost(origin) { return origin.includes('//') ? origin.split('/')[2].replace('*.', '') : origin; } let OptionsSyncPerDomain = (() => { let _instanceExtraInitializers = []; let _getOptionsForOrigin_decorators; let _getAllOrigins_decorators; return class OptionsSyncPerDomain { static { const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0; _getOptionsForOrigin_decorators = [memoizeDecorator()]; _getAllOrigins_decorators = [memoizeDecorator()]; __esDecorate(this, null, _getOptionsForOrigin_decorators, { kind: "method", name: "getOptionsForOrigin", static: false, private: false, access: { has: obj => "getOptionsForOrigin" in obj, get: obj => obj.getOptionsForOrigin }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _getAllOrigins_decorators, { kind: "method", name: "getAllOrigins", static: false, private: false, access: { has: obj => "getAllOrigins" in obj, get: obj => obj.getAllOrigins }, metadata: _metadata }, null, _instanceExtraInitializers); if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata }); } static migrations = OptionsSync.migrations; #defaultOrigins = (__runInitializers(this, _instanceExtraInitializers), patternToRegex(...normalizeManifestPermissions().origins)); #defaultOptions; #syncedForm; #changeEventTarget = new EventTarget(); constructor(options) { // Apply defaults this.#defaultOptions = { storageName: 'options', ...options, }; if (!isBackground()) { return; } // Run migrations for every origin if (options.migrations?.length) { this.getAllOrigins(); } // Delete stored options when permissions are removed chrome.permissions.onRemoved.addListener(({ origins }) => { const storageKeysToRemove = (origins ?? []) .filter(key => !this.#defaultOrigins.test(key)) .map(key => this.getStorageNameForOrigin(key)); chrome.storage.sync.remove(storageKeysToRemove); }); } getOptionsForOrigin(origin = location.origin) { // Extension pages should always use the default options as base if (!origin.startsWith('http') || this.#defaultOrigins.test(origin)) { return new OptionsSync(this.#defaultOptions); } return new OptionsSync({ ...this.#defaultOptions, storageName: this.getStorageNameForOrigin(origin), }); } async getAllOrigins() { if (isContentScript()) { throw new Error('This function only works on extension pages'); } const instances = new Map(); instances.set('default', this.getOptionsForOrigin()); for (const { domain, origin } of await this.getAdditionalOrigins()) { instances.set(domain, this.getOptionsForOrigin(origin)); } return instances; } async getAdditionalOrigins() { const { origins } = await queryAdditionalPermissions({ strictOrigins: false }); return origins.map(origin => ({ origin, domain: parseHost(origin), })); } async syncForm(form) { if (isContentScript()) { throw new Error('This function only works on extension pages'); } if (typeof form === 'string') { form = document.querySelector(form); } // Start synching the default options const currentOrigin = this.getOptionsForOrigin(); await currentOrigin.syncForm(form); this.#syncedForm = currentOrigin; // Look for other origins const additionalOrigins = await this.getAdditionalOrigins(); if (additionalOrigins.length === 0) { return Object.freeze({ domainCount: 1, getSelectedDomain: () => 'default', onChange() { }, }); } const allOrigins = ['default', ...additionalOrigins.map(item => item.domain)]; // Create domain picker const dropdown = document.createElement('select'); dropdown.addEventListener('change', this._domainChangeHandler); for (const domain of allOrigins) { const option = document.createElement('option'); // TODO: Use origin as value instead of domain, this makes things more consistent option.value = domain; option.textContent = domain; dropdown.append(option); } // Wrap and prepend to form const wrapper = document.createElement('p'); wrapper.append('Domain selector: ', dropdown); wrapper.classList.add('OptionsSyncPerDomain-picker'); form.prepend(wrapper, document.createElement('hr')); return Object.freeze({ domainCount: allOrigins.length, getSelectedDomain: () => dropdown.value, onChange: (callback) => { this.#changeEventTarget.addEventListener('change', () => { callback(dropdown.value); }); }, }); } getStorageNameForOrigin(origin) { return this.#defaultOptions.storageName + '-' + parseHost(origin); } getOriginFromDomain(storageName) { return storageName === 'default' ? undefined : 'https://*.' + storageName + '/*'; } _domainChangeHandler = async (event) => { const dropdown = event.currentTarget; const newOptions = this.getOptionsForOrigin(this.getOriginFromDomain(dropdown.value)); this.#syncedForm.stopSyncForm(); this.#syncedForm = newOptions; await newOptions.syncForm(dropdown.form); this.#changeEventTarget.dispatchEvent(new Event('change')); }; }; })(); export default OptionsSyncPerDomain;