GM_webextPref

A config library powered by webext-pref.

Detta skript bör inte installeras direkt. Det är ett bibliotek för andra skript att inkludera med meta-direktivet // @require https://update.greasyfork.org/scripts/371339/961539/GM_webextPref.js

// ==UserScript==
// @name gm-webext-pref
// @version 0.4.2
// @description A config library powered by webext-pref.
// @license MIT
// @author eight04 <eight04@gmail.com>
// @homepageURL https://github.com/eight04/GM_webextPref
// @supportURL https://github.com/eight04/GM_webextPref/issues
// @namespace eight04.blogspot.com
// @grant GM_getValue
// @grant GM.getValue
// @grant GM_setValue
// @grant GM.setValue
// @grant GM_deleteValue
// @grant GM.deleteValue
// @grant GM_addValueChangeListener
// @grant GM_registerMenuCommand
// @grant GM.registerMenuCommand
// @include *
// ==/UserScript==

var GM_webextPref = (function () {
  'use strict';

  /**
   * event-lite.js - Light-weight EventEmitter (less than 1KB when gzipped)
   *
   * @copyright Yusuke Kawasaki
   * @license MIT
   * @constructor
   * @see https://github.com/kawanet/event-lite
   * @see http://kawanet.github.io/event-lite/EventLite.html
   * @example
   * var EventLite = require("event-lite");
   *
   * function MyClass() {...}             // your class
   *
   * EventLite.mixin(MyClass.prototype);  // import event methods
   *
   * var obj = new MyClass();
   * obj.on("foo", function() {...});     // add event listener
   * obj.once("bar", function() {...});   // add one-time event listener
   * obj.emit("foo");                     // dispatch event
   * obj.emit("bar");                     // dispatch another event
   * obj.off("foo");                      // remove event listener
   */

  function EventLite() {
    if (!(this instanceof EventLite)) return new EventLite();
  }

  const _module_ = {exports: {}};
  (function(EventLite) {
    // export the class for node.js
    if ("undefined" !== typeof _module_) _module_.exports = EventLite;

    // property name to hold listeners
    var LISTENERS = "listeners";

    // methods to export
    var methods = {
      on: on,
      once: once,
      off: off,
      emit: emit
    };

    // mixin to self
    mixin(EventLite.prototype);

    // export mixin function
    EventLite.mixin = mixin;

    /**
     * Import on(), once(), off() and emit() methods into target object.
     *
     * @function EventLite.mixin
     * @param target {Prototype}
     */

    function mixin(target) {
      for (var key in methods) {
        target[key] = methods[key];
      }
      return target;
    }

    /**
     * Add an event listener.
     *
     * @function EventLite.prototype.on
     * @param type {string}
     * @param func {Function}
     * @returns {EventLite} Self for method chaining
     */

    function on(type, func) {
      getListeners(this, type).push(func);
      return this;
    }

    /**
     * Add one-time event listener.
     *
     * @function EventLite.prototype.once
     * @param type {string}
     * @param func {Function}
     * @returns {EventLite} Self for method chaining
     */

    function once(type, func) {
      var that = this;
      wrap.originalListener = func;
      getListeners(that, type).push(wrap);
      return that;

      function wrap() {
        off.call(that, type, wrap);
        func.apply(this, arguments);
      }
    }

    /**
     * Remove an event listener.
     *
     * @function EventLite.prototype.off
     * @param [type] {string}
     * @param [func] {Function}
     * @returns {EventLite} Self for method chaining
     */

    function off(type, func) {
      var that = this;
      var listners;
      if (!arguments.length) {
        delete that[LISTENERS];
      } else if (!func) {
        listners = that[LISTENERS];
        if (listners) {
          delete listners[type];
          if (!Object.keys(listners).length) return off.call(that);
        }
      } else {
        listners = getListeners(that, type, true);
        if (listners) {
          listners = listners.filter(ne);
          if (!listners.length) return off.call(that, type);
          that[LISTENERS][type] = listners;
        }
      }
      return that;

      function ne(test) {
        return test !== func && test.originalListener !== func;
      }
    }

    /**
     * Dispatch (trigger) an event.
     *
     * @function EventLite.prototype.emit
     * @param type {string}
     * @param [value] {*}
     * @returns {boolean} True when a listener received the event
     */

    function emit(type, value) {
      var that = this;
      var listeners = getListeners(that, type, true);
      if (!listeners) return false;
      var arglen = arguments.length;
      if (arglen === 1) {
        listeners.forEach(zeroarg);
      } else if (arglen === 2) {
        listeners.forEach(onearg);
      } else {
        var args = Array.prototype.slice.call(arguments, 1);
        listeners.forEach(moreargs);
      }
      return !!listeners.length;

      function zeroarg(func) {
        func.call(that);
      }

      function onearg(func) {
        func.call(that, value);
      }

      function moreargs(func) {
        func.apply(that, args);
      }
    }

    /**
     * @ignore
     */

    function getListeners(that, type, readonly) {
      if (readonly && !that[LISTENERS]) return;
      var listeners = that[LISTENERS] || (that[LISTENERS] = {});
      return listeners[type] || (listeners[type] = []);
    }

  })(EventLite);
  var EventLite$1 = _module_.exports;

  function createPref(DEFAULT, sep = "/") {
    let storage;
    let currentScope = "global";
    let scopeList = ["global"];
    const events = new EventLite$1;
    const globalCache = {};
    let scopedCache = {};
    let currentCache = Object.assign({}, DEFAULT);
    let initializing;
    
    return Object.assign(events, {
      // storage,
      // ready,
      connect,
      disconnect,
      get,
      getAll,
      set,
      getCurrentScope,
      setCurrentScope,
      addScope,
      deleteScope,
      getScopeList,
      import: import_,
      export: export_,
      has
    });
    
    function import_(input) {
      const newScopeList = input.scopeList || scopeList.slice();
      const scopes = new Set(newScopeList);
      if (!scopes.has("global")) {
        throw new Error("invalid scopeList");
      }
      const changes = {
        scopeList: newScopeList
      };
      for (const [scopeName, scope] of Object.entries(input.scopes)) {
        if (!scopes.has(scopeName)) {
          continue;
        }
        for (const [key, value] of Object.entries(scope)) {
          if (DEFAULT[key] == undefined) {
            continue;
          }
          changes[`${scopeName}${sep}${key}`] = value;
        }
      }
      return storage.setMany(changes);
    }
    
    function export_() {
      const keys = [];
      for (const scope of scopeList) {
        keys.push(...Object.keys(DEFAULT).map(k => `${scope}${sep}${k}`));
      }
      keys.push("scopeList");
      return storage.getMany(keys)
        .then(changes => {
          const _scopeList = changes.scopeList || scopeList.slice();
          const scopes = new Set(_scopeList);
          const output = {
            scopeList: _scopeList,
            scopes: {}
          };
          for (const [key, value] of Object.entries(changes)) {
            const sepIndex = key.indexOf(sep);
            if (sepIndex < 0) {
              continue;
            }
            const scope = key.slice(0, sepIndex);
            const realKey = key.slice(sepIndex + sep.length);
            if (!scopes.has(scope)) {
              continue;
            }
            if (DEFAULT[realKey] == undefined) {
              continue;
            }
            if (!output.scopes[scope]) {
              output.scopes[scope] = {};
            }
            output.scopes[scope][realKey] = value;
          }
          return output;
        });
    }
    
    function connect(_storage) {
      storage = _storage;
      initializing = storage.getMany(
        Object.keys(DEFAULT).map(k => `global${sep}${k}`).concat(["scopeList"])
      )
        .then(updateCache);
      storage.on("change", updateCache);
      return initializing;
    }
    
    function disconnect() {
      storage.off("change", updateCache);
      storage = null;
    }
    
    function updateCache(changes, rebuildCache = false) {
      if (changes.scopeList) {
        scopeList = changes.scopeList;
        events.emit("scopeListChange", scopeList);
        if (!scopeList.includes(currentScope)) {
          return setCurrentScope("global");
        }
      }
      const changedKeys = new Set;
      for (const [key, value] of Object.entries(changes)) {
        const [scope, realKey] = key.startsWith(`global${sep}`) ? ["global", key.slice(6 + sep.length)] :
          key.startsWith(`${currentScope}${sep}`) ? [currentScope, key.slice(currentScope.length + sep.length)] :
            [null, null];
        if (!scope || DEFAULT[realKey] == null) {
          continue;
        }
        if (scope === "global") {
          changedKeys.add(realKey);
          globalCache[realKey] = value;
        }
        if (scope === currentScope) {
          changedKeys.add(realKey);
          scopedCache[realKey] = value;
        }
      }
      if (rebuildCache) {
        Object.keys(DEFAULT).forEach(k => changedKeys.add(k));
      }
      const realChanges = {};
      let isChanged = false;
      for (const key of changedKeys) {
        const value = scopedCache[key] != null ? scopedCache[key] :
          globalCache[key] != null ? globalCache[key] :
          DEFAULT[key];
        if (currentCache[key] !== value) {
          realChanges[key] = value;
          currentCache[key] = value;
          isChanged = true;
        }
      }
      if (isChanged) {
        events.emit("change", realChanges);
      }
    }
    
    function has(key) {
      return currentCache.hasOwnProperty(key);
    }
    
    function get(key) {
      return currentCache[key];
    }
    
    function getAll() {
      return Object.assign({}, currentCache);
    }
    
    function set(key, value) {
      return storage.setMany({
        [`${currentScope}${sep}${key}`]: value
      });
    }
    
    function getCurrentScope() {
      return currentScope;
    }
    
    function setCurrentScope(newScope) {
      if (currentScope === newScope) {
        return Promise.resolve(true);
      }
      if (!scopeList.includes(newScope)) {
        return Promise.resolve(false);
      }
      return storage.getMany(Object.keys(DEFAULT).map(k => `${newScope}${sep}${k}`))
        .then(changes => {
          currentScope = newScope;
          scopedCache = {};
          events.emit("scopeChange", currentScope);
          updateCache(changes, true);
          return true;
        });
    }
    
    function addScope(scope) {
      if (scopeList.includes(scope)) {
        return Promise.reject(new Error(`${scope} already exists`));
      }
      if (scope.includes(sep)) {
        return Promise.reject(new Error(`invalid word: ${sep}`));
      }
      return storage.setMany({
        scopeList: scopeList.concat([scope])
      });
    }
    
    function deleteScope(scope) {
      if (scope === "global") {
        return Promise.reject(new Error(`cannot delete global`));
      }
      return Promise.all([
        storage.setMany({
          scopeList: scopeList.filter(s => s != scope)
        }),
        storage.deleteMany(Object.keys(DEFAULT).map(k => `${scope}${sep}${k}`))
      ]);
    }
    
    function getScopeList() {
      return scopeList;
    }
  }

  const keys = Object.keys;
  function isBoolean(val) {
    return typeof val === "boolean"
  }
  function isElement(val) {
    return val && typeof val.nodeType === "number"
  }
  function isString(val) {
    return typeof val === "string"
  }
  function isNumber(val) {
    return typeof val === "number"
  }
  function isObject(val) {
    return typeof val === "object" ? val !== null : isFunction(val)
  }
  function isFunction(val) {
    return typeof val === "function"
  }
  function isArrayLike(obj) {
    return isObject(obj) && typeof obj.length === "number" && typeof obj.nodeType !== "number"
  }
  function forEach(value, fn) {
    if (!value) return

    for (const key of keys(value)) {
      fn(value[key], key);
    }
  }
  function isRef(maybeRef) {
    return isObject(maybeRef) && "current" in maybeRef
  }

  function _objectWithoutPropertiesLoose(source, excluded) {
    if (source == null) return {}
    var target = {};
    var sourceKeys = Object.keys(source);
    var key, i;

    for (i = 0; i < sourceKeys.length; i++) {
      key = sourceKeys[i];
      if (excluded.indexOf(key) >= 0) continue
      target[key] = source[key];
    }

    return target
  }

  const isUnitlessNumber = {
    animationIterationCount: 0,
    borderImageOutset: 0,
    borderImageSlice: 0,
    borderImageWidth: 0,
    boxFlex: 0,
    boxFlexGroup: 0,
    boxOrdinalGroup: 0,
    columnCount: 0,
    columns: 0,
    flex: 0,
    flexGrow: 0,
    flexPositive: 0,
    flexShrink: 0,
    flexNegative: 0,
    flexOrder: 0,
    gridArea: 0,
    gridRow: 0,
    gridRowEnd: 0,
    gridRowSpan: 0,
    gridRowStart: 0,
    gridColumn: 0,
    gridColumnEnd: 0,
    gridColumnSpan: 0,
    gridColumnStart: 0,
    fontWeight: 0,
    lineClamp: 0,
    lineHeight: 0,
    opacity: 0,
    order: 0,
    orphans: 0,
    tabSize: 0,
    widows: 0,
    zIndex: 0,
    zoom: 0,
    fillOpacity: 0,
    floodOpacity: 0,
    stopOpacity: 0,
    strokeDasharray: 0,
    strokeDashoffset: 0,
    strokeMiterlimit: 0,
    strokeOpacity: 0,
    strokeWidth: 0,
  };

  function prefixKey(prefix, key) {
    return prefix + key.charAt(0).toUpperCase() + key.substring(1)
  }

  const prefixes = ["Webkit", "ms", "Moz", "O"];
  keys(isUnitlessNumber).forEach((prop) => {
    prefixes.forEach((prefix) => {
      isUnitlessNumber[prefixKey(prefix, prop)] = 0;
    });
  });

  const SVGNamespace = "http://www.w3.org/2000/svg";
  const XLinkNamespace = "http://www.w3.org/1999/xlink";
  const XMLNamespace = "http://www.w3.org/XML/1998/namespace";

  function isVisibleChild(value) {
    return !isBoolean(value) && value != null
  }

  function className(value) {
    if (Array.isArray(value)) {
      return value.map(className).filter(Boolean).join(" ")
    } else if (isObject(value)) {
      return keys(value)
        .filter((k) => value[k])
        .join(" ")
    } else if (isVisibleChild(value)) {
      return "" + value
    } else {
      return ""
    }
  }
  const svg = {
    animate: 0,
    circle: 0,
    clipPath: 0,
    defs: 0,
    desc: 0,
    ellipse: 0,
    feBlend: 0,
    feColorMatrix: 0,
    feComponentTransfer: 0,
    feComposite: 0,
    feConvolveMatrix: 0,
    feDiffuseLighting: 0,
    feDisplacementMap: 0,
    feDistantLight: 0,
    feFlood: 0,
    feFuncA: 0,
    feFuncB: 0,
    feFuncG: 0,
    feFuncR: 0,
    feGaussianBlur: 0,
    feImage: 0,
    feMerge: 0,
    feMergeNode: 0,
    feMorphology: 0,
    feOffset: 0,
    fePointLight: 0,
    feSpecularLighting: 0,
    feSpotLight: 0,
    feTile: 0,
    feTurbulence: 0,
    filter: 0,
    foreignObject: 0,
    g: 0,
    image: 0,
    line: 0,
    linearGradient: 0,
    marker: 0,
    mask: 0,
    metadata: 0,
    path: 0,
    pattern: 0,
    polygon: 0,
    polyline: 0,
    radialGradient: 0,
    rect: 0,
    stop: 0,
    svg: 0,
    switch: 0,
    symbol: 0,
    text: 0,
    textPath: 0,
    tspan: 0,
    use: 0,
    view: 0,
  };
  function createElement(tag, attr, ...children) {
    if (isString(attr) || Array.isArray(attr)) {
      children.unshift(attr);
      attr = {};
    }

    attr = attr || {};

    if (!attr.namespaceURI && svg[tag] === 0) {
      attr = Object.assign({}, attr, {
        namespaceURI: SVGNamespace,
      });
    }

    if (attr.children != null && !children.length) {
      var _attr = attr
      ;({ children } = _attr);
      attr = _objectWithoutPropertiesLoose(_attr, ["children"]);
    }

    let node;

    if (isString(tag)) {
      node = attr.namespaceURI
        ? document.createElementNS(attr.namespaceURI, tag)
        : document.createElement(tag);
      attributes(attr, node);
      appendChild(children, node);
    } else if (isFunction(tag)) {
      if (isObject(tag.defaultProps)) {
        attr = Object.assign({}, tag.defaultProps, attr);
      }

      node = tag(
        Object.assign({}, attr, {
          children,
        })
      );
    }

    if (isRef(attr.ref)) {
      attr.ref.current = node;
    } else if (isFunction(attr.ref)) {
      attr.ref(node);
    }

    return node
  }

  function appendChild(child, node) {
    if (isArrayLike(child)) {
      appendChildren(child, node);
    } else if (isString(child) || isNumber(child)) {
      appendChildToNode(document.createTextNode(child), node);
    } else if (child === null) {
      appendChildToNode(document.createComment(""), node);
    } else if (isElement(child)) {
      appendChildToNode(child, node);
    }
  }

  function appendChildren(children, node) {
    for (const child of children) {
      appendChild(child, node);
    }

    return node
  }

  function appendChildToNode(child, node) {
    if (node instanceof window.HTMLTemplateElement) {
      node.content.appendChild(child);
    } else {
      node.appendChild(child);
    }
  }

  function normalizeAttribute(s) {
    return s.replace(/[A-Z\d]/g, (match) => ":" + match.toLowerCase())
  }

  function attribute(key, value, node) {
    switch (key) {
      case "xlinkActuate":
      case "xlinkArcrole":
      case "xlinkHref":
      case "xlinkRole":
      case "xlinkShow":
      case "xlinkTitle":
      case "xlinkType":
        attrNS(node, XLinkNamespace, normalizeAttribute(key), value);
        return

      case "xmlnsXlink":
        attr(node, normalizeAttribute(key), value);
        return

      case "xmlBase":
      case "xmlLang":
      case "xmlSpace":
        attrNS(node, XMLNamespace, normalizeAttribute(key), value);
        return
    }

    switch (key) {
      case "htmlFor":
        attr(node, "for", value);
        return

      case "dataset":
        forEach(value, (dataValue, dataKey) => {
          if (dataValue != null) {
            node.dataset[dataKey] = dataValue;
          }
        });
        return

      case "innerHTML":
      case "innerText":
      case "textContent":
        node[key] = value;
        return

      case "spellCheck":
        node.spellcheck = value;
        return

      case "class":
      case "className":
        if (isFunction(value)) {
          value(node);
        } else {
          attr(node, "class", className(value));
        }

        return

      case "ref":
      case "namespaceURI":
        return

      case "style":
        if (isObject(value)) {
          forEach(value, (val, key) => {
            if (isNumber(val) && isUnitlessNumber[key] !== 0) {
              node.style[key] = val + "px";
            } else {
              node.style[key] = val;
            }
          });
          return
        }
    }

    if (isFunction(value)) {
      if (key[0] === "o" && key[1] === "n") {
        node[key.toLowerCase()] = value;
      }
    } else if (value === true) {
      attr(node, key, "");
    } else if (value !== false && value != null) {
      attr(node, key, value);
    }
  }

  function attr(node, key, value) {
    node.setAttribute(key, value);
  }

  function attrNS(node, namespace, key, value) {
    node.setAttributeNS(namespace, key, value);
  }

  function attributes(attr, node) {
    for (const key of keys(attr)) {
      attribute(key, attr[key], node);
    }

    return node
  }

  function messageGetter({
    getMessage,
    DEFAULT
  }) {
    return (key, params) => {
      const message = getMessage(key, params);
      if (message) return message;
      const defaultMessage = DEFAULT[key];
      if (!defaultMessage) return "";
      if (!params) return defaultMessage;

      if (!Array.isArray(params)) {
        params = [params];
      }

      return defaultMessage.replace(/\$(\d+)/g, (m, n) => params[n - 1]);
    };
  }

  function fallback(getMessage) {
    return messageGetter({
      getMessage,
      DEFAULT: {
        currentScopeLabel: "Current scope",
        addScopeLabel: "Add new scope",
        deleteScopeLabel: "Delete current scope",
        learnMoreButton: "Learn more",
        importButton: "Import",
        exportButton: "Export",
        addScopePrompt: "Add new scope",
        deleteScopeConfirm: "Delete scope $1?",
        importPrompt: "Paste settings",
        exportPrompt: "Copy settings"
      }
    });
  }

  const VALID_CONTROL = new Set(["import", "export", "scope-list", "add-scope", "delete-scope"]);

  class DefaultMap extends Map {
    constructor(getDefault) {
      super();
      this.getDefault = getDefault;
    }

    get(key) {
      let item = super.get(key);

      if (!item) {
        item = this.getDefault();
        super.set(key, item);
      }

      return item;
    }

  }

  function bindInputs(pref, inputs) {
    const bounds = [];

    const onPrefChange = change => {
      for (const key in change) {
        if (!inputs.has(key)) {
          continue;
        }

        for (const input of inputs.get(key)) {
          updateInput(input, change[key]);
        }
      }
    };

    pref.on("change", onPrefChange);
    bounds.push(() => pref.off("change", onPrefChange));

    for (const [key, list] of inputs.entries()) {
      for (const input of list) {
        const evt = input.hasAttribute("realtime") ? "input" : "change";

        const onChange = () => updatePref(key, input);

        input.addEventListener(evt, onChange);
        bounds.push(() => input.removeEventListener(evt, onChange));
      }
    }

    onPrefChange(pref.getAll());
    return () => {
      for (const unbind of bounds) {
        unbind();
      }
    };

    function updatePref(key, input) {
      if (!input.checkValidity()) {
        return;
      }

      if (input.type === "checkbox") {
        pref.set(key, input.checked);
        return;
      }

      if (input.type === "radio") {
        if (input.checked) {
          pref.set(key, input.value);
        }

        return;
      }

      if (input.nodeName === "SELECT" && input.multiple) {
        pref.set(key, [...input.options].filter(o => o.selected).map(o => o.value));
        return;
      }

      if (input.type === "number" || input.type === "range") {
        pref.set(key, Number(input.value));
        return;
      }

      pref.set(key, input.value);
    }

    function updateInput(input, value) {
      if (input.nodeName === "INPUT" && input.type === "radio") {
        input.checked = input.value === value;
        return;
      }

      if (input.type === "checkbox") {
        input.checked = value;
        return;
      }

      if (input.nodeName === "SELECT" && input.multiple) {
        const checked = new Set(value);

        for (const option of input.options) {
          option.selected = checked.has(option.value);
        }

        return;
      }

      input.value = value;
    }
  }

  function bindFields(pref, fields) {
    const onPrefChange = change => {
      for (const key in change) {
        if (!fields.has(key)) {
          continue;
        }

        for (const field of fields.get(key)) {
          field.disabled = field.dataset.bindToValue ? field.dataset.bindToValue !== change[key] : !change[key];
        }
      }
    };

    pref.on("change", onPrefChange);
    onPrefChange(pref.getAll());
    return () => pref.off("change", onPrefChange);
  }

  function bindControls({
    pref,
    controls,
    alert: _alert = alert,
    confirm: _confirm = confirm,
    prompt: _prompt = prompt,
    getMessage = () => {},
    getNewScope = () => ""
  }) {
    const CONTROL_METHODS = {
      "import": ["click", doImport],
      "export": ["click", doExport],
      "scope-list": ["change", updateCurrentScope],
      "add-scope": ["click", addScope],
      "delete-scope": ["click", deleteScope]
    };

    for (const type in CONTROL_METHODS) {
      for (const el of controls.get(type)) {
        el.addEventListener(CONTROL_METHODS[type][0], CONTROL_METHODS[type][1]);
      }
    }

    pref.on("scopeChange", updateCurrentScopeEl);
    pref.on("scopeListChange", updateScopeList);
    updateScopeList();
    updateCurrentScopeEl();

    const _ = fallback(getMessage);

    return unbind;

    function unbind() {
      pref.off("scopeChange", updateCurrentScopeEl);
      pref.off("scopeListChange", updateScopeList);

      for (const type in CONTROL_METHODS) {
        for (const el of controls.get(type)) {
          el.removeEventListener(CONTROL_METHODS[type][0], CONTROL_METHODS[type][1]);
        }
      }
    }

    async function doImport() {
      try {
        const input = await _prompt(_("importPrompt"));

        if (input == null) {
          return;
        }

        const settings = JSON.parse(input);
        return pref.import(settings);
      } catch (err) {
        await _alert(err.message);
      }
    }

    async function doExport() {
      try {
        const settings = await pref.export();
        await _prompt(_("exportPrompt"), JSON.stringify(settings));
      } catch (err) {
        await _alert(err.message);
      }
    }

    function updateCurrentScope(e) {
      pref.setCurrentScope(e.target.value);
    }

    async function addScope() {
      try {
        let scopeName = await _prompt(_("addScopePrompt"), getNewScope());

        if (scopeName == null) {
          return;
        }

        scopeName = scopeName.trim();

        if (!scopeName) {
          throw new Error("the value is empty");
        }

        await pref.addScope(scopeName);
        pref.setCurrentScope(scopeName);
      } catch (err) {
        await _alert(err.message);
      }
    }

    async function deleteScope() {
      try {
        const scopeName = pref.getCurrentScope();
        const result = await _confirm(_("deleteScopeConfirm", scopeName));

        if (result) {
          return pref.deleteScope(scopeName);
        }
      } catch (err) {
        await _alert(err.message);
      }
    }

    function updateCurrentScopeEl() {
      const scopeName = pref.getCurrentScope();

      for (const el of controls.get("scope-list")) {
        el.value = scopeName;
      }
    }

    function updateScopeList() {
      const scopeList = pref.getScopeList();

      for (const el of controls.get("scope-list")) {
        el.innerHTML = "";
        el.append(...scopeList.map(scope => {
          const option = document.createElement("option");
          option.value = scope;
          option.textContent = scope;
          return option;
        }));
      }
    }
  }

  function createBinding({
    pref,
    root,
    elements = root.querySelectorAll("input, textarea, select, fieldset, button"),
    keyPrefix = "pref-",
    controlPrefix = "webext-pref-",
    alert,
    confirm,
    prompt,
    getMessage,
    getNewScope
  }) {
    const inputs = new DefaultMap(() => []);
    const fields = new DefaultMap(() => []);
    const controls = new DefaultMap(() => []);

    for (const element of elements) {
      const id = element.id && stripPrefix(element.id, keyPrefix);

      if (id && pref.has(id)) {
        inputs.get(id).push(element);
        continue;
      }

      if (element.nodeName === "INPUT" && element.type === "radio") {
        const name = element.name && stripPrefix(element.name, keyPrefix);

        if (name && pref.has(name)) {
          inputs.get(name).push(element);
          continue;
        }
      }

      if (element.nodeName === "FIELDSET" && element.dataset.bindTo) {
        fields.get(element.dataset.bindTo).push(element);
        continue;
      }

      const controlType = findControlType(element.classList);

      if (controlType) {
        controls.get(controlType).push(element);
      }
    }

    const bounds = [bindInputs(pref, inputs), bindFields(pref, fields), bindControls({
      pref,
      controls,
      alert,
      confirm,
      prompt,
      getMessage,
      getNewScope
    })];
    return () => {
      for (const unbind of bounds) {
        unbind();
      }
    };

    function stripPrefix(id, prefix) {
      if (!prefix) {
        return id;
      }

      return id.startsWith(prefix) ? id.slice(prefix.length) : "";
    }

    function findControlType(list) {
      for (const name of list) {
        const controlType = stripPrefix(name, controlPrefix);

        if (VALID_CONTROL.has(controlType)) {
          return controlType;
        }
      }
    }
  }

  function createUI({
    body,
    getMessage = () => {},
    toolbar = true,
    navbar = true,
    keyPrefix = "pref-",
    controlPrefix = "webext-pref-"
  }) {
    const root = document.createDocumentFragment();

    const _ = fallback(getMessage);

    if (toolbar) {
      root.append(createToolbar());
    }

    if (navbar) {
      root.append(createNavbar());
    }

    root.append( /*#__PURE__*/createElement("div", {
      class: controlPrefix + "body"
    }, body.map(item => {
      if (!item.hLevel) {
        item.hLevel = 3;
      }

      return createItem(item);
    })));
    return root;

    function createToolbar() {
      return /*#__PURE__*/createElement("div", {
        class: controlPrefix + "toolbar"
      }, /*#__PURE__*/createElement("button", {
        type: "button",
        class: [controlPrefix + "import", "browser-style"]
      }, _("importButton")), /*#__PURE__*/createElement("button", {
        type: "button",
        class: [controlPrefix + "export", "browser-style"]
      }, _("exportButton")));
    }

    function createNavbar() {
      return /*#__PURE__*/createElement("div", {
        class: controlPrefix + "nav"
      }, /*#__PURE__*/createElement("select", {
        class: [controlPrefix + "scope-list", "browser-style"],
        title: _("currentScopeLabel")
      }), /*#__PURE__*/createElement("button", {
        type: "button",
        class: [controlPrefix + "delete-scope", "browser-style"],
        title: _("deleteScopeLabel")
      }, "\xD7"), /*#__PURE__*/createElement("button", {
        type: "button",
        class: [controlPrefix + "add-scope", "browser-style"],
        title: _("addScopeLabel")
      }, "+"));
    }

    function createItem(p) {
      if (p.type === "section") {
        return createSection(p);
      }

      if (p.type === "checkbox") {
        return createCheckbox(p);
      }

      if (p.type === "radiogroup") {
        return createRadioGroup(p);
      }

      return createInput(p);
    }

    function createInput(p) {
      const key = keyPrefix + p.key;
      let input;
      const onChange = p.validate ? e => {
        try {
          p.validate(e.target.value);
          e.target.setCustomValidity("");
        } catch (err) {
          e.target.setCustomValidity(err.message || String(err));
        }
      } : null;

      if (p.type === "select") {
        input = /*#__PURE__*/createElement("select", {
          multiple: p.multiple,
          class: "browser-style",
          id: key,
          onChange: onChange
        }, Object.entries(p.options).map(([value, label]) => /*#__PURE__*/createElement("option", {
          value: value
        }, label)));
      } else if (p.type === "textarea") {
        input = /*#__PURE__*/createElement("textarea", {
          rows: "8",
          class: "browser-style",
          id: key,
          onChange: onChange
        });
      } else {
        input = /*#__PURE__*/createElement("input", {
          type: p.type,
          id: key,
          onChange: onChange
        });
      }

      return /*#__PURE__*/createElement("div", {
        class: [`${controlPrefix}${p.type}`, "browser-style", p.className]
      }, /*#__PURE__*/createElement("label", {
        htmlFor: key
      }, p.label), p.learnMore && /*#__PURE__*/createElement(LearnMore, {
        url: p.learnMore
      }), input, p.help && /*#__PURE__*/createElement(Help, {
        content: p.help
      }));
    }

    function createRadioGroup(p) {
      return /*#__PURE__*/createElement("div", {
        class: [`${controlPrefix}${p.type}`, "browser-style", p.className]
      }, /*#__PURE__*/createElement("div", {
        class: controlPrefix + "radio-title"
      }, p.label), p.learnMore && /*#__PURE__*/createElement(LearnMore, {
        url: p.learnMore
      }), p.help && /*#__PURE__*/createElement(Help, {
        content: p.help
      }), p.children.map(c => {
        c.parentKey = p.key;
        return createCheckbox(inheritProp(p, c));
      }));
    }

    function Help({
      content
    }) {
      return /*#__PURE__*/createElement("p", {
        class: controlPrefix + "help"
      }, content);
    }

    function LearnMore({
      url
    }) {
      return /*#__PURE__*/createElement("a", {
        href: url,
        class: controlPrefix + "learn-more",
        target: "_blank",
        rel: "noopener noreferrer"
      }, _("learnMoreButton"));
    }

    function createCheckbox(p) {
      const id = p.parentKey ? `${keyPrefix}${p.parentKey}-${p.value}` : keyPrefix + p.key;
      return /*#__PURE__*/createElement("div", {
        class: [`${controlPrefix}${p.type}`, "browser-style", p.className]
      }, /*#__PURE__*/createElement("input", {
        type: p.type,
        id: id,
        name: p.parentKey ? keyPrefix + p.parentKey : null,
        value: p.value
      }), /*#__PURE__*/createElement("label", {
        htmlFor: id
      }, p.label), p.learnMore && /*#__PURE__*/createElement(LearnMore, {
        url: p.learnMore
      }), p.help && /*#__PURE__*/createElement(Help, {
        content: p.help
      }), p.children && /*#__PURE__*/createElement("fieldset", {
        class: controlPrefix + "checkbox-children",
        dataset: {
          bindTo: p.parentKey || p.key,
          bindToValue: p.value
        }
      }, p.children.map(c => createItem(inheritProp(p, c)))));
    }

    function createSection(p) {
      const Header = `h${p.hLevel}`;
      p.hLevel++;
      return (
        /*#__PURE__*/
        // FIXME: do we need browser-style for section?
        createElement("div", {
          class: [controlPrefix + p.type, p.className]
        }, /*#__PURE__*/createElement(Header, {
          class: controlPrefix + "header"
        }, p.label), p.help && /*#__PURE__*/createElement(Help, {
          content: p.help
        }), p.children && p.children.map(c => createItem(inheritProp(p, c))))
      );
    }

    function inheritProp(parent, child) {
      child.hLevel = parent.hLevel;
      return child;
    }
  }

  /* eslint-env greasemonkey */

  function createGMStorage() {
    const setValue = typeof GM_setValue === "function" ?
      promisify(GM_setValue) : GM.setValue.bind(GM);
    const getValue = typeof GM_getValue === "function" ?
      promisify(GM_getValue) : GM.getValue.bind(GM);
    const deleteValue = typeof GM_deleteValue === "function" ?
      promisify(GM_deleteValue) : GM.deleteValue.bind(GM);
    const events = new EventLite$1;
    
    if (typeof GM_addValueChangeListener === "function") {
      GM_addValueChangeListener("webext-pref-message", (name, oldValue, newValue) => {
        const changes = JSON.parse(newValue);
        for (const key of Object.keys(changes)) {
          if (typeof changes[key] === "object" && changes[key].$undefined) {
            changes[key] = undefined;
          }
        }
        events.emit("change", changes);
      });
    }
    
    return Object.assign(events, {getMany, setMany, deleteMany});
    
    function getMany(keys) {
      return Promise.all(keys.map(k => 
        getValue(`webext-pref/${k}`)
          .then(value => [k, typeof value === "string" ? JSON.parse(value) : value])
      ))
        .then(entries => {
          const output = {};
          for (const [key, value] of entries) {
            output[key] = value;
          }
          return output;
        });
    }
    
    function setMany(changes) {
      return Promise.all(Object.entries(changes).map(([key, value]) => 
        setValue(`webext-pref/${key}`, JSON.stringify(value))
      ))
        .then(() => {
          if (typeof GM_addValueChangeListener === "function") {
            return setValue("webext-pref-message", JSON.stringify(changes));
          }
          events.emit("change", changes);
        });
    }
    
    function deleteMany(keys) {
      return Promise.all(keys.map(k => deleteValue(`webext-pref/${k}`)))
        .then(() => {
          if (typeof GM_addValueChangeListener === "function") {
            const changes = {};
            for (const key of keys) {
              changes[key] = {
                $undefined: true
              };
            }
            return setValue("webext-pref-message", JSON.stringify(changes));
          }
          const changes = {};
          for (const key of keys) {
            changes[key] = undefined;
          }
          events.emit("change", changes);
        });
    }
    
    function promisify(fn) {
      return (...args) => {
        try {
          return Promise.resolve(fn(...args));
        } catch (err) {
          return Promise.reject(err);
        }
      };
    }
  }

  /* eslint-env greasemonkey */

  function GM_webextPref({
    default: default_,
    separator,
    css = "",
    ...options
  }) {
    const pref = createPref(default_, separator);
    const initializing = pref.connect(createGMStorage());
    let isOpen = false;
    
    const registerMenu = 
      typeof GM_registerMenuCommand === "function" ? GM_registerMenuCommand :
      typeof GM !== "undefined" && GM && GM.registerMenuCommand ? GM.registerMenuCommand.bind(GM) :
      undefined;
      
    if (registerMenu) {
      registerMenu(`${getTitle()} - Configure`, openDialog);
    }
    
    return Object.assign(pref, {
      ready: () => initializing,
      openDialog
    });
    
    function openDialog() {
      if (isOpen) {
        return;
      }
      isOpen = true;
      
      let destroyView;
      
      const modal = document.createElement("div");
      modal.className = "webext-pref-modal";
      modal.onclick = () => {
        modal.classList.remove("webext-pref-modal-open");
        modal.addEventListener("transitionend", () => {
          if (destroyView) {
            destroyView();
          }
          modal.remove();
          isOpen = false;
        });
      };
      
      const style = document.createElement("style");
      style.textContent = "body{overflow:hidden}.webext-pref-modal{position:fixed;top:0;right:0;bottom:0;left:0;background:rgba(0,0,0,.5);overflow:auto;z-index:999999;opacity:0;transition:opacity .2s linear;display:flex}.webext-pref-modal-open{opacity:1}.webext-pref-modal::after,.webext-pref-modal::before{content:\"\";display:block;height:30px;visibility:hidden}.webext-pref-iframe-wrap{margin:auto}.webext-pref-iframe{margin:30px 0;display:inline-block;width:100%;max-width:100%;background:#fff;border-width:0;box-shadow:0 0 30px #000;transform:translateY(-20px);transition:transform .2s linear}.webext-pref-modal-open .webext-pref-iframe{transform:none}" + `
      body {
        padding-right: ${window.innerWidth - document.documentElement.offsetWidth}px;
      }
    `;
      
      const iframe = document.createElement("iframe");
      iframe.className = "webext-pref-iframe";
      iframe.srcdoc = `
      <html>
        <head>
          <style class="dialog-style"></style>
        </head>
        <body>
          <div class="dialog-body"></div>
        </body>
      </html>
    `;
      
      const wrap = document.createElement("div");
      wrap.className = "webext-pref-iframe-wrap";
      
      wrap.append(iframe);
      modal.append(style, wrap);
      document.body.appendChild(modal);
      
      iframe.onload = () => {
        iframe.onload = null;
        
        iframe.contentDocument.querySelector(".dialog-style").textContent = "body{display:inline-block;font-size:16px;font-family:sans-serif;white-space:nowrap;overflow:hidden;margin:0;color:#3d3d3d;line-height:1}input[type=number],input[type=text],select,textarea{display:block;width:100%;box-sizing:border-box;height:2em;font:inherit;padding:0 .3em;border:1px solid #9e9e9e;cursor:pointer}select[multiple],textarea{height:6em}input[type=number]:hover,input[type=text]:hover,select:hover,textarea:hover{border-color:#d5d5d5}input[type=number]:focus,input[type=text]:focus,select:focus,textarea:focus{cursor:auto;border-color:#3a93ee}textarea{line-height:1.5}input[type=checkbox],input[type=radio]{display:inline-block;width:1em;height:1em;font:inherit;margin:0}button{box-sizing:border-box;height:2em;font:inherit;border:1px solid #9e9e9e;cursor:pointer;background:0 0}button:hover{border-color:#d5d5d5}button:focus{border-color:#3a93ee}.dialog-body{margin:2em}.webext-pref-toolbar{display:flex;align-items:center;margin-bottom:1em}.dialog-title{font-size:1.34em;margin:0 2em 0 0;flex-grow:1}.webext-pref-toolbar button{font-size:.7em;margin-left:.5em}.webext-pref-nav{display:flex;margin-bottom:1em}.webext-pref-nav select{text-align:center;text-align-last:center}.webext-pref-nav button{width:2em}.webext-pref-number,.webext-pref-radiogroup,.webext-pref-select,.webext-pref-text,.webext-pref-textarea{margin:1em 0}.webext-pref-body>:first-child{margin-top:0}.webext-pref-body>:last-child{margin-bottom:0}.webext-pref-number>input,.webext-pref-select>select,.webext-pref-text>input,.webext-pref-textarea>textarea{margin:.3em 0}.webext-pref-checkbox,.webext-pref-radio{margin:.5em 0;padding-left:1.5em}.webext-pref-checkbox>input,.webext-pref-radio>input{margin-left:-1.5em;margin-right:.5em;vertical-align:middle}.webext-pref-checkbox>label,.webext-pref-radio>label{cursor:pointer;vertical-align:middle}.webext-pref-checkbox>label:hover,.webext-pref-radio>label:hover{color:#707070}.webext-pref-checkbox-children,.webext-pref-radio-children{margin:.7em 0 0;padding:0;border-width:0}.webext-pref-checkbox-children[disabled],.webext-pref-radio-children[disabled]{opacity:.5}.webext-pref-checkbox-children>:first-child,.webext-pref-radio-children>:first-child{margin-top:0}.webext-pref-checkbox-children>:last-child,.webext-pref-radio-children>:last-child{margin-bottom:0}.webext-pref-checkbox-children>:last-child>:last-child,.webext-pref-radio-children>:last-child>:last-child{margin-bottom:0}.webext-pref-help{color:#969696}.responsive{white-space:normal}.responsive .dialog-body{margin:1em}.responsive .webext-pref-toolbar{display:block}.responsive .dialog-title{margin:0 0 1em 0}.responsive .webext-pref-toolbar button{font-size:1em}.responsive .webext-pref-nav{display:block}" + css;
        
        const root = iframe.contentDocument.querySelector(".dialog-body");
        root.append(createUI(options));
        
        destroyView = createBinding({
          pref,
          root,
          ...options
        });
        
        const title = document.createElement("h2");
        title.className = "dialog-title";
        title.textContent = getTitle();
        iframe.contentDocument.querySelector(".webext-pref-toolbar").prepend(title);
        
        if (iframe.contentDocument.body.offsetWidth > modal.offsetWidth) {
          iframe.contentDocument.body.classList.add("responsive");
        }
        
        // calc iframe size
        iframe.style = `
        width: ${iframe.contentDocument.body.offsetWidth}px;
        height: ${iframe.contentDocument.body.scrollHeight}px;
      `;
        
        modal.classList.add("webext-pref-modal-open");
      };
    }
    
    function getTitle() {
      return typeof GM_info === "object" ?
        GM_info.script.name : GM.info.script.name;
    }
  }

  return GM_webextPref;

}());