webext-options-sync-per-domain
Version:
Helps you manage and autosave your extension's options, separately for each additional permission.
142 lines (141 loc) • 6.06 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { memoizeDecorator } from 'memoize';
import { patternToRegex } from 'webext-patterns';
import OptionsSync from 'webext-options-sync';
import { isBackgroundPage, 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;
}
export default class OptionsSyncPerDomain {
static migrations = OptionsSync.migrations;
#defaultOrigins = patternToRegex(...normalizeManifestPermissions().origins);
#defaultOptions;
#syncedForm;
#changeEventTarget = new EventTarget();
constructor(options) {
// Apply defaults
this.#defaultOptions = {
storageName: 'options',
...options,
};
if (!isBackgroundPage()) {
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: async (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;
this.#syncedForm.stopSyncForm();
await this.getOptionsForOrigin(this.getOriginFromDomain(dropdown.value)).syncForm(dropdown.form);
this.#changeEventTarget.dispatchEvent(new Event('change'));
};
}
__decorate([
memoizeDecorator()
], OptionsSyncPerDomain.prototype, "getOptionsForOrigin", null);
__decorate([
memoizeDecorator()
], OptionsSyncPerDomain.prototype, "getAllOrigins", null);