GM_webextPref

A config library powered by webext-pref.

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://greasyfork.org/scripts/371339-gm-webextpref/code/GM_webextPref.js?version=705415

// ==UserScript==
// @name GM_webextPref
// @version 0.3.0
// @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
// @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_
    });
    
    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 ready() {
      return initializing;
    }
    
    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;
    }
  }

  /* global chrome */

  /* global browser chrome */

  function promisify(fn) {
    return (...args) => {
      try {
        return Promise.resolve(fn(...args));
      } catch (err) {
        return Promise.reject(err);
      }
    };
  }

  function createView({
    root,
    pref,
    body,
    getMessage = () => {},
    getNewScope = () => "",
    prompt: _prompt = promisify(prompt),
    alert: _alert = promisify(alert),
    confirm: _confirm = promisify(confirm)
  }) {
    getMessage = createGetMessage(getMessage);
    const toolbar = createToolbar();
    const nav = createNav();
    const form = createForm(body);
    
    root.append(toolbar.frag, nav.frag, form.frag);
    
    pref.on("scopeListChange", nav.updateScopeList);
    nav.updateScopeList(pref.getScopeList());
    
    pref.on("scopeChange", nav.updateScope);
    nav.updateScope(pref.getCurrentScope());
    
    pref.on("change", form.updateInputs);
    form.updateInputs(pref.getAll());
    
    return destroy;
    
    function createGetMessage(userGetMessage) {
      const DEFAULT = {
        currentScopeLabel: "Current scope",
        addScopeLabel: "Add new scope",
        addScopePrompt: "Add new scope",
        deleteScopeLabel: "Delete current scope",
        deleteScopeConfirm: scope => `Delete scope ${scope}?`,
        learnMoreButton: "Learn more",
        importButton: "Import",
        exportButton: "Export",
        importPrompt: "Paste settings",
        exportPrompt: "Copy settings"
      };
      return (key, params) => {
        const message = userGetMessage(key, params);
        if (message) {
          return message;
        }
        if (typeof DEFAULT[key] === "function") {
          return DEFAULT[key](params);
        }
        return DEFAULT[key];
      };
    }
    
    function createToolbar() {
      const container = document.createElement("div");
      container.className = "webext-pref-toolbar";
      
      const importButton = document.createElement("button");
      importButton.className = "webext-pref-import browser-style";
      importButton.type = "button";
      importButton.textContent = getMessage("importButton");
      importButton.onclick = () => {
        _prompt(getMessage("importPrompt"))
          .then(input => {
            if (input == null) {
              return;
            }
            const settings = JSON.parse(input);
            return pref.import(settings);
          })
          .catch(err => _alert(err.message));
      };
      
      const exportButton = document.createElement("button");
      exportButton.className = "webext-pref-export browser-style";
      exportButton.type = "button";
      exportButton.textContent = getMessage("exportButton");
      exportButton.onclick = () => {
        pref.export()
          .then(settings =>
            _prompt(getMessage("exportPrompt"), JSON.stringify(settings))
          )
          .catch(err => _alert(err.message));
      };
      
      container.append(importButton, exportButton);
      return {frag: container};
    }
    
    function destroy() {
      root.innerHTML = "";
      pref.off("scopeChange", nav.updateScope);
      pref.off("scopeListChange", nav.updateScopeList);
      pref.off("change", form.updateInputs);
    }
    
    function createForm(body) {
      const container = document.createElement("div");
      container.className = "webext-pref-body";
      
      const _body = createBody({body});
      container.append(..._body.frag);
      
      return Object.assign(_body, {updateInputs, frag: container});
      
      function updateInputs(changes) {
        for (const [key, value] of Object.entries(changes)) {
          if (_body.inputs[key]) {
            _body.inputs[key].setValue(value);
          }
        }
      }
    }
    
    function createNav() {
      const container = document.createElement("div");
      container.className = "webext-pref-nav";
      
      const select = document.createElement("select");
      select.className = "browser-style";
      select.title = getMessage("currentScopeLabel");
      select.onchange = () => {
        pref.setCurrentScope(select.value);
      };
      
      const deleteButton = document.createElement("button");
      deleteButton.className = "browser-style webext-pref-delete-scope";
      deleteButton.title = getMessage("deleteScopeLabel");
      deleteButton.type = "button";
      deleteButton.textContent = "×";
      deleteButton.onclick = () => {
        const scopeName = pref.getCurrentScope();
        _confirm(getMessage("deleteScopeConfirm", scopeName))
          .then(result => {
            if (result) {
              return pref.deleteScope(scopeName);
            }
          })
          .catch(err => _alert(err.message));
      };
      
      const addButton = document.createElement("button");
      addButton.className = "browser-style webext-pref-add-scope";
      addButton.title = getMessage("addScopeLabel");
      addButton.type = "button";
      addButton.textContent = "+";
      addButton.onclick = () => {
        _prompt(getMessage("addScopePrompt"), getNewScope())
          .then(scopeName => {
            if (scopeName == null) {
              return;
            }
            scopeName = scopeName.trim();
            if (!scopeName) {
              throw new Error("the value is empty");
            }
            return pref.addScope(scopeName)
              .then(() => pref.setCurrentScope(scopeName));
          })
          .catch(err => {
            _alert(err.message);
          });
      };
      
      container.append(select, deleteButton, addButton);
      
      return {
        updateScope,
        updateScopeList,
        frag: container
      };
      
      function updateScope(newScope) {
        select.value = newScope;
      }
      
      function updateScopeList(scopeList) {
        select.innerHTML = "";
        select.append(...scopeList.map(scope => {
          const option = document.createElement("option");
          option.value = scope;
          option.textContent = scope;
          return option;
        }));
      }
    }
    
    function createBody({body, hLevel = 3}) {
      const inputs = {};
      const frag = [];
      for (const el of body) {
        const container = document.createElement("div");
        container.className = `webext-pref-${el.type} browser-style`;
        Object.assign(
          el,
          el.type === "section" ? createSection({el, hLevel}) :
            el.type === "checkbox" ? createCheckbox(el) :
            el.type === "radiogroup" ? createRadioGroup(el) :
            createInput(el)
        );
        if (el.key) {
          inputs[el.key] = el;
        }
        if (el.inputs) {
          Object.assign(inputs, el.inputs);
        }
        container.append(...el.frag);
        frag.push(container);
      }
      return {
        inputs,
        frag
      };
    }
    
    function createSection({el, hLevel}) {
      const header = document.createElement(`h${hLevel}`);
      header.className = "webext-pref-header";
      header.textContent = el.label;
      
      const body = createBody({
        body: el.children,
        hLevel: hLevel + 1
      });
      
      const frag = [header];
      if (el.help) {
        frag.push(createHelp(el.help));
      }
      frag.push(...body.frag);
      
      return {
        inputs: body.inputs,
        frag
      };
    }
    
    function createCheckbox(el) {
      const input = document.createElement("input");
      const inputs = {};
      input.id = `pref-${el.key}`;
      input.type = el.type;
      if (el.type === "checkbox") {
        // checkbox
        input.onchange = () => {
          pref.set(el.key, input.checked);
        };
      } else {
        // radio
        input.name = `pref-${el.parentKey}`;
        input.onchange = () => {
          if (input.checked) {
            pref.set(el.parentKey, el.value);
          }
        };
      }
      
      const frag = [
        input,
        createLabel(el.label, input.id)
      ];
      
      if (el.learnMore) {
        frag.push(createLearnMore(el.learnMore));
      }
      if (el.help) {
        frag.push(createHelp(el.help));
      }
      const childContainer = el.children && el.children.length ?
        createChildContainer(el.children) : null;
      if (childContainer) {
        frag.push(childContainer);
      }
      
      return {
        inputs,
        frag,
        setValue: value => {
          input.checked = value;
          if (childContainer) {
            childContainer.disabled = !input.checked;
          }
        }
      };
      
      function createChildContainer(children) {
        const container = document.createElement("fieldset");
        container.className = "webext-pref-checkbox-children";
        container.disabled = true;
        const body = createBody({body: children});
        container.append(...body.frag);
        Object.assign(inputs, body.inputs);
        return container;
      }
    }
    
    function createRadioGroup(el) {
      const frag = [];
      const inputs = {};
      
      const radios = el.children.map(option => {
        const container = document.createElement("div");
        container.className = "webext-pref-checkbox browser-style";
        
        const checkbox = createCheckbox(Object.assign({}, option, {
          key: `${el.key}-${option.value}`,
          parentKey: el.key
        }));
        
        Object.assign(inputs, checkbox.inputs);
        
        container.append(...checkbox.frag);
        return Object.assign(checkbox, {frag: container, key: option.value});
      });
      
      const radioMap = {};
      for (const radio of radios) {
        radioMap[radio.key] = radio;
      }
      
      if (el.label) {
        frag.push(createTitle(el.label));
      }
      if (el.learnMore) {
        frag.push(createLearnMore(el.learnMore));
      }
      if (el.help) {
        frag.push(createHelp(el.help));
      }
      frag.push(...radios.map(r => r.frag));
      
      let currentValue;
      
      return {
        inputs,
        frag,
        setValue: value => {
          if (currentValue) {
            radioMap[currentValue].setValue(false);
          }
          radioMap[value].setValue(true);
          currentValue = value;
        }
      };
      
      function createTitle(text) {
        const title = document.createElement("span");
        title.className = "webext-pref-radio-title";
        title.textContent = text;
        return title;
      }
    }
    
    function createInput(el) {
      const frag = [];
      let input;
      if (el.type === "select") {
        input = document.createElement("select");
        input.className = "browser-style";
        input.multiple = el.multiple;
        input.append(...Object.entries(el.options).map(([key, value]) => {
          const option = document.createElement("option");
          option.value = key;
          option.textContent = value;
          return option;
        }));
      } else if (el.type === "textarea") {
        input = document.createElement("textarea");
        input.rows = "8";
        input.className = "browser-style";
      } else {
        input = document.createElement("input");
        input.type = el.type;
      }
      input.id = `pref-${el.key}`;
      input.onchange = () => {
        const value = el.type === "select" && el.multiple ?
          [...input.selectedOptions].map(i => i.value) :
          el.type === "number" ? Number(input.value) : input.value;
        if (el.validate) {
          let err;
          try {
            el.validate(value);
          } catch (_err) {
            err = _err;
          }
          input.setCustomValidity(err ? err.message : "");
          if (err) {
            return;
          }
        }
        pref.set(el.key, value);
      };
      
      frag.push(createLabel(el.label, input.id));
      if (el.learnMore) {
        frag.push(createLearnMore(el.learnMore));
      }
      frag.push(input);
      if (el.help) {
        frag.push(createHelp(el.help));
      }
      
      return {
        frag,
        setValue: value => {
          if (el.type !== "select" || !el.multiple) {
            input.value = value;
          } else {
            const set = new Set(value);
            for (const option of input.options) {
              option.selected = set.has(option.value);
            }
          }
          if (el.validate) {
            input.setCustomValidity("");
          }
        }
      };
    }
    
    function createHelp(text) {
      const help = document.createElement("div");
      help.className = "webext-pref-help";
      help.textContent = text;
      return help;
    }
    
    function createLabel(text, id) {
      const label = document.createElement("label");
      label.textContent = text;
      label.htmlFor = id;
      return label;
    }
    
    function createLearnMore(url) {
      const a = document.createElement("a");
      a.className = "webext-pref-learn-more";
      a.href = url;
      a.target = "_blank";
      a.rel = "noopener";
      a.textContent = getMessage("learnMoreButton");
      return a;
    }
  }

  /* 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,
    body,
    getNewScope,
    getMessage,
    alert,
    confirm,
    prompt
  }) {
    const pref = createPref(_default);
    const initializing = pref.connect(createGMStorage());
    let isOpen = false;
    
    if (typeof GM_registerMenuCommand === "function") {
      GM_registerMenuCommand(`${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 = e => {
        if (e.target === modal) {
          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;text-align:center}.webext-pref-modal-open{opacity:1}.webext-pref-modal::before{content:\"\";display:inline-block;height:100%;vertical-align:middle}.webext-pref-iframe{display:inline-block;vertical-align:middle;width:90%;background:#fff;margin:30px 0;box-shadow:0 0 30px #000;border-width:0;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>
    `;
      
      modal.append(style, iframe);
      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-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{margin:.5em 0;padding-left:1.5em}.webext-pref-checkbox>input{margin-left:-1.5em;margin-right:.5em;vertical-align:middle}.webext-pref-checkbox>label{cursor:pointer;vertical-align:middle}.webext-pref-checkbox>label:hover{color:#707070}.webext-pref-checkbox-children{margin:.7em 0 0;padding:0;border-width:0}.webext-pref-checkbox-children[disabled]{opacity:.5}.webext-pref-checkbox-children>:first-child{margin-top:0}.webext-pref-checkbox-children>:last-child{margin-bottom:0}.webext-pref-checkbox-children>:last-child>:last-child{margin-bottom:0}.webext-pref-help{color:#969696}";
        
        destroyView = createView({
          pref,
          body,
          root: iframe.contentDocument.querySelector(".dialog-body"),
          getNewScope,
          getMessage,
          alert,
          confirm,
          prompt
        });
        
        const title = document.createElement("h2");
        title.className = "dialog-title";
        title.textContent = getTitle();
        iframe.contentDocument.querySelector(".webext-pref-toolbar").prepend(title);
        
        // 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;

}());
X