Bläddra i källkod

refactor: rewrite options in ES6

Gerald 8 år sedan
förälder
incheckning
f37dc800ac
63 ändrade filer med 2374 tillägg och 2244 borttagningar
  1. 1 0
      .eslintignore
  2. 56 0
      .eslintrc.js
  3. 0 43
      .eslintrc.yml
  4. 1 0
      .gitignore
  5. 10 1
      package.json
  6. 62 0
      scripts/utils.js
  7. 10 0
      scripts/vue-loader.conf.js
  8. 103 0
      scripts/webpack.conf.js
  9. 16 0
      src/.babelrc
  10. 0 191
      src/common.js
  11. 108 0
      src/common/index.js
  12. 40 0
      src/common/options.js
  13. 43 0
      src/common/polyfills.js
  14. 1 1
      src/manifest.json
  15. 95 101
      src/options/app.js
  16. 22 0
      src/options/style.css
  17. 11 8
      src/options/utils/dropdown.js
  18. 37 31
      src/options/utils/features.js
  19. 28 4
      src/options/utils/index.js
  20. 22 19
      src/options/utils/settings.js
  21. 99 0
      src/options/views/code.vue
  22. 0 28
      src/options/views/confirm.html
  23. 0 194
      src/options/views/confirm.js
  24. 202 0
      src/options/views/confirm.vue
  25. 0 120
      src/options/views/edit.html
  26. 0 300
      src/options/views/edit.js
  27. 408 0
      src/options/views/edit.vue
  28. 0 1
      src/options/views/editor.html
  29. 0 150
      src/options/views/editor.js
  30. 0 15
      src/options/views/main.html
  31. 0 23
      src/options/views/main.js
  32. 44 0
      src/options/views/main.vue
  33. 0 4
      src/options/views/message.html
  34. 0 26
      src/options/views/message.js
  35. 18 0
      src/options/views/message.vue
  36. 255 0
      src/options/views/script-item.vue
  37. 0 27
      src/options/views/script.html
  38. 0 254
      src/options/views/script.js
  39. 0 28
      src/options/views/tab-about.html
  40. 0 12
      src/options/views/tab-about.js
  41. 43 0
      src/options/views/tab-about.vue
  42. 0 17
      src/options/views/tab-installed.html
  43. 0 85
      src/options/views/tab-installed.js
  44. 101 0
      src/options/views/tab-installed.vue
  45. 0 35
      src/options/views/tab-settings/index.html
  46. 0 23
      src/options/views/tab-settings/index.js
  47. 61 0
      src/options/views/tab-settings/index.vue
  48. 37 0
      src/options/views/tab-settings/vm-blacklist.vue
  49. 0 8
      src/options/views/tab-settings/vm-blacklist/index.html
  50. 0 25
      src/options/views/tab-settings/vm-blacklist/index.js
  51. 32 0
      src/options/views/tab-settings/vm-css.vue
  52. 0 8
      src/options/views/tab-settings/vm-css/index.html
  53. 0 20
      src/options/views/tab-settings/vm-css/index.js
  54. 142 0
      src/options/views/tab-settings/vm-export.vue
  55. 0 13
      src/options/views/tab-settings/vm-export/index.html
  56. 0 132
      src/options/views/tab-settings/vm-export/index.js
  57. 137 0
      src/options/views/tab-settings/vm-import.vue
  58. 0 5
      src/options/views/tab-settings/vm-import/index.html
  59. 0 131
      src/options/views/tab-settings/vm-import/index.js
  60. 129 0
      src/options/views/tab-settings/vm-sync.vue
  61. 0 24
      src/options/views/tab-settings/vm-sync/index.html
  62. 0 116
      src/options/views/tab-settings/vm-sync/index.js
  63. 0 21
      src/public/mylib/CodeMirror/fold.css

+ 1 - 0
.eslintignore

@@ -0,0 +1 @@
+scripts/**

+ 56 - 0
.eslintrc.js

@@ -0,0 +1,56 @@
+// http://eslint.org/docs/user-guide/configuring
+
+module.exports = {
+  root: true,
+  parser: 'babel-eslint',
+  parserOptions: {
+    sourceType: 'module'
+  },
+  env: {
+    browser: true,
+  },
+  extends: 'airbnb-base',
+  // required to lint *.vue files
+  plugins: [
+    'html'
+  ],
+  // check if imports actually resolve
+  'settings': {
+    'import/resolver': {
+      'webpack': {
+        'config': 'scripts/webpack.conf.js'
+      }
+    }
+  },
+  // add your custom rules here
+  'rules': {
+    // don't require .vue extension when importing
+    'import/extensions': ['error', 'always', {
+      'js': 'never',
+      'vue': 'never'
+    }],
+    // allow optionalDependencies
+    'import/no-extraneous-dependencies': ['error', {
+      'optionalDependencies': ['test/unit/index.js']
+    }],
+    // allow debugger during development
+    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
+    'no-console': ['error', {
+      allow: ['error', 'warn'],
+    }],
+    'no-param-reassign': ['error', {
+      props: false,
+    }],
+    'array-callback-return': ['off'],
+    'consistent-return': ['off'],
+    'no-use-before-define': ['error', 'nofunc'],
+    'object-shorthand': ['error', 'always'],
+    'no-mixed-operators': ['error', {allowSamePrecedence: true}],
+    'no-bitwise': ['error', {int32Hint: true}],
+    'no-underscore-dangle': ['off'],
+  },
+  globals: {
+    browser: true,
+    zip: true,
+  },
+}

+ 0 - 43
.eslintrc.yml

@@ -1,43 +0,0 @@
-rules:
-  indent:
-    - 2
-    - 2
-  quotes:
-    - 2
-    - single
-  linebreak-style:
-    - 2
-    - unix
-  semi:
-    - 2
-    - always
-  comma-dangle:
-    - 0
-  no-console:
-    - 2
-    - allow:
-      - warn
-      - error
-  no-unused-vars:
-    - 2
-    - args: all
-      argsIgnorePattern: ^_
-  keyword-spacing:
-    - 2
-  space-before-function-paren:
-    - 2
-    - anonymous: always
-      named: never
-
-env:
-  browser: true
-  node: true
-
-globals:
-  zip: true
-  Vue: true
-  CodeMirror: true
-  Promise: true
-  browser: true
-
-extends: 'eslint:recommended'

+ 1 - 0
.gitignore

@@ -1,5 +1,6 @@
 dist/
 node_modules/
+*.zip
 *.nex
 *.crx
 *.log

+ 10 - 1
package.json

@@ -13,9 +13,17 @@
   },
   "description": "Violentmonkey",
   "devDependencies": {
+    "babel-eslint": "^7.2.0",
     "cssnano": "^3.10.0",
     "del": "^2.2.0",
     "eslint": "^3.17.1",
+    "eslint-config-airbnb-base": "^11.1.1",
+    "eslint-friendly-formatter": "^2.0.7",
+    "eslint-import-resolver-webpack": "^0.8.1",
+    "eslint-plugin-html": "^2.0.1",
+    "eslint-plugin-import": "^2.2.0",
+    "extract-text-webpack-plugin": "^2.1.0",
+    "friendly-errors-webpack-plugin": "^1.6.1",
     "glob": "^7.0.3",
     "gulp": "^3.9.1",
     "gulp-concat": "^2.6.0",
@@ -32,7 +40,8 @@
     "ncp": "^2.0.0",
     "precss": "^1.4.0",
     "svgo": "^0.7.2",
-    "through2": "^2.0.3"
+    "through2": "^2.0.3",
+    "webpack": "^2.3.1"
   },
   "author": "Gerald <[email protected]>",
   "repository": {

+ 62 - 0
scripts/utils.js

@@ -0,0 +1,62 @@
+const ExtractTextPlugin = require('extract-text-webpack-plugin');
+
+exports.cssLoaders = function (options) {
+  options = options || {}
+
+  var cssLoader = {
+    loader: 'css-loader',
+    options: {
+      minimize: process.env.NODE_ENV === 'production',
+      sourceMap: options.sourceMap
+    }
+  }
+
+  // generate loader string to be used with extract text plugin
+  function generateLoaders (loader, loaderOptions) {
+    var loaders = [cssLoader]
+    if (loader) {
+      loaders.push({
+        loader: loader + '-loader',
+        options: Object.assign({}, loaderOptions, {
+          sourceMap: options.sourceMap
+        })
+      })
+    }
+
+    // Extract CSS when that option is specified
+    // (which is the case during production build)
+    if (options.extract) {
+      return ExtractTextPlugin.extract({
+        use: loaders,
+        fallback: 'vue-style-loader'
+      })
+    } else {
+      return ['vue-style-loader'].concat(loaders)
+    }
+  }
+
+  // http://vuejs.github.io/vue-loader/en/configurations/extract-css.html
+  return {
+    css: generateLoaders(),
+    postcss: generateLoaders(),
+    less: generateLoaders('less'),
+    sass: generateLoaders('sass', { indentedSyntax: true }),
+    scss: generateLoaders('sass'),
+    stylus: generateLoaders('stylus'),
+    styl: generateLoaders('stylus')
+  }
+}
+
+// Generate loaders for standalone style files (outside of .vue)
+exports.styleLoaders = function (options) {
+  var output = []
+  var loaders = exports.cssLoaders(options)
+  for (var extension in loaders) {
+    var loader = loaders[extension]
+    output.push({
+      test: new RegExp('\\.' + extension + '$'),
+      use: loader
+    })
+  }
+  return output
+}

+ 10 - 0
scripts/vue-loader.conf.js

@@ -0,0 +1,10 @@
+const utils = require('./utils');
+const isProduction = process.env.NODE_ENV === 'production';
+
+module.exports = {
+  loaders: utils.cssLoaders({
+    sourceMap: false,
+    extract: isProduction,
+  }),
+  postcss: [require('precss')],
+};

+ 103 - 0
scripts/webpack.conf.js

@@ -0,0 +1,103 @@
+const path = require('path');
+const webpack = require('webpack');
+const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin');
+const ExtractTextPlugin = require('extract-text-webpack-plugin');
+const utils = require('./utils');
+const vueLoaderConfig = require('./vue-loader.conf');
+const IS_DEV = process.env.NODE_ENV === 'development';
+const DIST = 'dist';
+
+function resolve (dir) {
+  return path.join(__dirname, '..', dir)
+}
+
+module.exports = {
+  entry: {
+    'background/app.js': './src/background/app.js',
+    'options/app.js': './src/options/app.js',
+    'popup/app.js': './src/popup/app.js',
+  },
+  output: {
+    path: DIST,
+    publicPath: '/',
+    filename: '[name].js',
+  },
+  resolve: {
+    extensions: ['.js', '.vue', '.json'],
+    alias: {
+      src: resolve('src'),
+    }
+  },
+  module: {
+    rules: [
+      {
+        test: /\.(js|vue)$/,
+        loader: 'eslint-loader',
+        enforce: 'pre',
+        include: [resolve('src'), resolve('test')],
+        options: {
+          formatter: require('eslint-friendly-formatter')
+        }
+      },
+      {
+        test: /\.vue$/,
+        loader: 'vue-loader',
+        options: vueLoaderConfig
+      },
+      {
+        test: /\.js$/,
+        loader: 'babel-loader',
+        include: [resolve('src'), resolve('test')]
+      },
+    ].concat(utils.styleLoaders({
+      sourceMap: false,
+      extract: !IS_DEV,
+    })),
+  },
+  // cheap-module-eval-source-map is faster for development
+  devtool: IS_DEV ? '#cheap-module-eval-source-map' : false,
+  plugins: [
+    new webpack.DefinePlugin({
+      'process.env': {},
+    }),
+    // split vendor js into its own file
+    new webpack.optimize.CommonsChunkPlugin({
+      name: 'vendor',
+    }),
+    new FriendlyErrorsPlugin(),
+    ... IS_DEV ? [
+      // https://github.com/ampedandwired/html-webpack-plugin
+      // new HtmlWebpackPlugin({
+      //   filename: 'index.html',
+      //   template: 'src/public/index.ejs',
+      //   inject: true,
+      //   config,
+      // })
+    ] : [
+      // extract css into its own file
+      new ExtractTextPlugin(`${DIST}/[name].css`),
+      // generate dist index.html with correct asset hash for caching.
+      // you can customize output by editing /index.html
+      // see https://github.com/ampedandwired/html-webpack-plugin
+      // new HtmlWebpackPlugin({
+      //   filename: 'index.html',
+      //   template: 'src/public/index.ejs',
+      //   inject: true,
+      //   minify: {
+      //     removeComments: true,
+      //     collapseWhitespace: true,
+      //     removeAttributeQuotes: true
+      //     // more options:
+      //     // https://github.com/kangax/html-minifier#options-quick-reference
+      //   },
+      //   // necessary to consistently work with multiple chunks via CommonsChunkPlugin
+      //   chunksSortMode: 'dependency'
+      // }),
+      new webpack.optimize.UglifyJsPlugin({
+        compress: {
+          warnings: false
+        }
+      }),
+    ],
+  ],
+};

+ 16 - 0
src/.babelrc

@@ -0,0 +1,16 @@
+{
+  "presets": [
+    ["latest", {
+      "es2015": { "modules": false }
+    }],
+    "stage-2"
+  ],
+  "plugins": ["transform-runtime"],
+  "comments": false,
+  "env": {
+    "test": {
+      "presets": ["latest", "stage-2"],
+      "plugins": [ "istanbul" ]
+    }
+  }
+}

+ 0 - 191
src/common.js

@@ -1,191 +0,0 @@
-// Polyfill start
-
-function polyfill(obj, name, value) {
-  if (!obj[name]) Object.defineProperty(obj, name, {
-    value: value,
-  });
-}
-
-polyfill(Object, 'assign', function () {
-  var obj = arguments[0];
-  for (var i = 1; i < arguments.length; i ++) {
-    var arg = arguments[i];
-    arg && Object.keys(arg).forEach(function (key) {
-      obj[key] = arg[key];
-    });
-  }
-  return obj;
-});
-polyfill(String.prototype, 'startsWith', function (str) {
-  return this.slice(0, str.length) === str;
-});
-polyfill(String.prototype, 'endsWith', function (str) {
-  return this.slice(-str.length) === str;
-});
-polyfill(Array.prototype, 'findIndex', function (predicate) {
-  var length = this.length;
-  for (var i = 0; i < length; i ++) {
-    var item = this[i];
-    if (predicate(item, i, this)) return i;
-  }
-  return -1;
-});
-polyfill(Array.prototype, 'find', function (predicate) {
-  return this[this.findIndex(predicate)];
-});
-
-// Polyfill end
-
-var _ = exports;
-
-_.i18n = function (name, args) {
-  return browser.i18n.getMessage(name, args) || name;
-};
-_.defaultImage = '/public/images/icon128.png';
-
-function normalizeKeys(key) {
-  if (!key) key = [];
-  if (!Array.isArray(key)) key = key.toString().split('.');
-  return key;
-}
-
-_.normalizeKeys = normalizeKeys;
-
-_.object = function () {
-  function get(obj, key, def) {
-    var keys = normalizeKeys(key);
-    for (var i = 0, len = keys.length; i < len; i ++) {
-      key = keys[i];
-      if (obj && typeof obj === 'object' && (key in obj)) obj = obj[key];
-      else return def;
-    }
-    return obj;
-  }
-  function set(obj, key, val) {
-    var keys = normalizeKeys(key);
-    if (!keys.length) return val;
-    var sub = obj = obj || {};
-    for (var i = 0, len = keys.length - 1; i < len; i ++) {
-      key = keys[i];
-      sub = sub[key] = sub[key] || {};
-    }
-    var lastKey = keys[keys.length - 1];
-    if (val == null) {
-      delete sub[lastKey];
-    } else {
-      sub[lastKey] = val;
-    }
-    return obj;
-  }
-  return {
-    get: get,
-    set: set,
-  };
-}();
-
-_.initHooks = function () {
-  var hooks = [];
-
-  function fire(data) {
-    hooks.slice().forEach(function (hook) {
-      hook(data);
-    });
-  }
-
-  function hook(callback) {
-    hooks.push(callback);
-    return function () {
-      var i = hooks.indexOf(callback);
-      ~i && hooks.splice(i, 1);
-    };
-  }
-
-  return {
-    hook: hook,
-    fire: fire,
-  };
-};
-
-_.initOptions = function () {
-  var options = {};
-  var hooks = _.initHooks();
-  var ready = _.sendMessage({cmd: 'GetAllOptions'})
-  .then(function (data) {
-    options = data;
-    data && hooks.fire(data);
-  });
-
-  function getOption(key, def) {
-    var keys = normalizeKeys(key);
-    return _.object.get(options, keys, def);
-  }
-
-  function setOption(key, value) {
-    _.sendMessage({
-      cmd: 'SetOptions',
-      data: {
-        key: key,
-        value: value,
-      },
-    });
-  }
-
-  function updateOptions(data) {
-    Object.keys(data).forEach(function (key) {
-      _.object.set(options, key, data[key]);
-    });
-    hooks.fire(data);
-  }
-
-  _.options = {
-    get: getOption,
-    set: setOption,
-    update: updateOptions,
-    hook: hooks.hook,
-    ready: ready,
-  };
-};
-
-_.sendMessage = function (data) {
-  return browser.runtime.sendMessage(data)
-  .catch(function (err) {
-    console.error(err);
-  });
-};
-
-_.debounce = function (func, time) {
-  function run(thisObj, args) {
-    timer = null;
-    func.apply(thisObj, args);
-  }
-  var timer;
-  return function () {
-    timer && clearTimeout(timer);
-    var args = [];
-    for (var i = 0; i < arguments.length; i++) args.push(arguments[i]);
-    timer = setTimeout(run, time, this, args);
-  };
-};
-
-_.noop = function () {};
-
-_.zfill = function (num, length) {
-  num = num.toString();
-  while (num.length < length) num = '0' + num;
-  return num;
-};
-
-_.getUniqId = function () {
-  return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
-};
-
-/**
- * Get locale attributes such as `@name:zh-CN`
- */
-_.getLocaleString = function (meta, key) {
-  var lang = navigator.languages.find(function (lang) {
-    return (key + ':' + lang) in meta;
-  });
-  if (lang) key += ':' + lang;
-  return meta[key] || '';
-};

+ 108 - 0
src/common/index.js

@@ -0,0 +1,108 @@
+import './polyfills';
+
+export function i18n(name, args) {
+  return browser.i18n.getMessage(name, args) || name;
+}
+export const defaultImage = '/public/images/icon128.png';
+
+export function normalizeKeys(key) {
+  let keys = key || [];
+  if (!Array.isArray(keys)) keys = keys.toString().split('.');
+  return keys;
+}
+
+export const object = {
+  get(obj, rawKey, def) {
+    const keys = normalizeKeys(rawKey);
+    let res = obj;
+    keys.some((key) => {
+      if (res && typeof res === 'object' && (key in res)) {
+        res = res[key];
+      } else {
+        res = def;
+        return true;
+      }
+    });
+    return res;
+  },
+  set(obj, rawKey, val) {
+    const keys = normalizeKeys(rawKey);
+    if (!keys.length) return val;
+    const root = obj || {};
+    let sub = root;
+    const lastKey = keys.pop();
+    keys.forEach((key) => {
+      let child = sub[key];
+      if (!child) {
+        child = {};
+        sub[key] = child;
+      }
+      sub = child;
+    });
+    if (val == null) {
+      delete sub[lastKey];
+    } else {
+      sub[lastKey] = val;
+    }
+    return obj;
+  },
+};
+
+export function initHooks() {
+  const hooks = [];
+
+  function fire(data) {
+    hooks.slice().forEach((cb) => {
+      cb(data);
+    });
+  }
+
+  function hook(callback) {
+    hooks.push(callback);
+    return () => {
+      const i = hooks.indexOf(callback);
+      if (i >= 0) hooks.splice(i, 1);
+    };
+  }
+
+  return { hook, fire };
+}
+
+export function sendMessage(data) {
+  return browser.runtime.sendMessage(data)
+  .catch((err) => {
+    console.error(err);
+  });
+}
+
+export function debounce(func, time) {
+  let timer;
+  function run(thisObj, args) {
+    timer = null;
+    func.apply(thisObj, args);
+  }
+  return function debouncedFunction(...args) {
+    if (timer) clearTimeout(timer);
+    timer = setTimeout(run, time, this, args);
+  };
+}
+
+export function noop() {}
+
+export function zfill(input, length) {
+  let num = input.toString();
+  while (num.length < length) num = `0${num}`;
+  return num;
+}
+
+export function getUniqId() {
+  return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
+}
+
+/**
+ * Get locale attributes such as `@name:zh-CN`
+ */
+export function getLocaleString(meta, key) {
+  const langKey = navigator.languages.map(lang => `${key}:${lang}`).find(item => item in meta);
+  return (langKey && meta[langKey]) || '';
+}

+ 40 - 0
src/common/options.js

@@ -0,0 +1,40 @@
+import { initHooks, sendMessage, object, normalizeKeys } from '.';
+
+let options = {};
+const hooks = initHooks();
+const ready = sendMessage({ cmd: 'GetAllOptions' })
+.then((data) => {
+  options = data;
+  if (data) hooks.fire(data);
+});
+
+function getOption(key, def) {
+  const keys = normalizeKeys(key);
+  return object.get(options, keys, def);
+}
+
+function setOption(key, value) {
+  sendMessage({
+    cmd: 'SetOptions',
+    data: { key, value },
+  });
+}
+
+function updateOptions(data) {
+  Object.keys(data).forEach((key) => {
+    object.set(options, key, data[key]);
+  });
+  hooks.fire(data);
+}
+
+function onReady(cb) {
+  ready.then(cb);
+}
+
+export default {
+  ready: onReady,
+  get: getOption,
+  set: setOption,
+  update: updateOptions,
+  hook: hooks.hook,
+};

+ 43 - 0
src/common/polyfills.js

@@ -0,0 +1,43 @@
+function polyfill(obj, name, value) {
+  if (!obj[name]) {
+    Object.defineProperty(obj, name, { value });
+  }
+}
+
+polyfill(Object, 'assign', (obj, ...args) => {
+  args.forEach(arg => arg && Object.keys(arg).forEach((key) => {
+    obj[key] = arg[key];
+  }));
+  return obj;
+});
+
+polyfill(String.prototype, 'startsWith', function startsWith(str) {
+  return this.slice(0, str.length) === str;
+});
+
+polyfill(String.prototype, 'endsWith', function endsWith(str) {
+  return this.slice(-str.length) === str;
+});
+
+polyfill(String.prototype, 'includes', function includes(str) {
+  return this.indexOf(str) >= 0;
+});
+
+polyfill(Array.prototype, 'findIndex', function findIndex(predicate) {
+  let index = -1;
+  this.some((item, i, thisObj) => {
+    if (predicate(item, i, thisObj)) {
+      index = i;
+      return true;
+    }
+  });
+  return index;
+});
+
+polyfill(Array.prototype, 'find', function find(predicate) {
+  return this[this.findIndex(predicate)];
+});
+
+polyfill(Array.prototype, 'includes', function includes(item) {
+  return this.indexOf(item) >= 0;
+});

+ 1 - 1
src/manifest.json

@@ -43,5 +43,5 @@
     "webRequestBlocking",
     "notifications"
   ],
-  "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'"
+  "content_security_policy": "script-src 'self'; object-src 'self'"
 }

+ 95 - 101
src/options/app.js

@@ -1,131 +1,125 @@
+import Vue from 'vue';
+import { sendMessage, i18n } from 'src/common';
+import options from 'src/common/options';
+import { store, features } from './utils';
+import Main from './views/main';
+import Confirm from './views/confirm';
+import './style.css';
+
+Object.assign(store, {
+  loading: false,
+  cache: {},
+  scripts: [],
+  sync: [],
+  route: null,
+});
+const handlers = {
+  UpdateOptions(data) {
+    options.update(data);
+  },
+};
+browser.runtime.onMessage.addListener((res) => {
+  const handle = handlers[res.cmd];
+  if (handle) handle(res.data);
+});
+zip.workerScriptsPath = '/public/lib/zip.js/';
+document.title = i18n('extName');
+initCustomCSS();
+
+const routes = {
+  '': {
+    comp: Main,
+    init: initMain,
+  },
+  confirm: {
+    comp: Confirm,
+  },
+};
+window.addEventListener('hashchange', loadHash, false);
+loadHash();
+
+options.ready(() => new Vue({
+  el: '#app',
+  // store.route.comp should not change
+  render: h => h(store.route.comp),
+}));
+
+function parseLocation(pathInfo) {
+  const [path, qs] = pathInfo.split('?');
+  const query = (qs || '').split('&').reduce((res, seq) => {
+    if (seq) {
+      const [key, val] = seq.split('=');
+      res[decodeURIComponent(key)] = decodeURIComponent(val);
+    }
+    return res;
+  }, {});
+  return { path, query };
+}
+function loadHash() {
+  const loc = parseLocation(location.hash.slice(1));
+  const route = routes[loc.path];
+  if (route) {
+    store.route = {
+      comp: route.comp,
+      query: loc.query,
+    };
+    if (route.init) {
+      route.init();
+      route.init = null;
+    }
+  } else {
+    location.hash = '';
+  }
+}
+
 function initMain() {
   store.loading = true;
-  _.sendMessage({cmd: 'GetData'})
-  .then(function (data) {
+  sendMessage({ cmd: 'GetData' })
+  .then((data) => {
     [
       'cache',
       'scripts',
       'sync',
-    ].forEach(function (key) {
+    ].forEach((key) => {
       Vue.set(store, key, data[key]);
     });
     store.loading = false;
-    // utils.features.reset(data.version);
-    utils.features.reset('sync');
+    // features.reset(data.version);
+    features.reset('sync');
   });
   Object.assign(handlers, {
-    UpdateSync: function (data) {
+    UpdateSync(data) {
       store.sync = data;
     },
-    AddScript: function (data) {
+    AddScript(data) {
       data.message = '';
       store.scripts.push(data);
     },
-    UpdateScript: function (data) {
+    UpdateScript(data) {
       if (!data) return;
-      var script = store.scripts.find(function (script) {
-        return script.id === data.id;
-      });
-      script && Object.keys(data).forEach(function (key) {
-        Vue.set(script, key, data[key]);
-      });
+      const script = store.scripts.find(item => item.id === data.id);
+      if (script) {
+        Object.keys(data).forEach((key) => {
+          Vue.set(script, key, data[key]);
+        });
+      }
     },
-    RemoveScript: function (data) {
-      var i = store.scripts.findIndex(function (script) {
-        return script.id === data;
-      });
-      ~i && store.scripts.splice(i, 1);
+    RemoveScript(data) {
+      const i = store.scripts.findIndex(script => script.id === data);
+      if (i >= 0) store.scripts.splice(i, 1);
     },
   });
 }
-function parseLocation(pathInfo) {
-  var parts = pathInfo.split('?');
-  var path = parts[0];
-  var query = (parts[1] || '').split('&').reduce(function (res, seq) {
-    if (seq) {
-      var parts = seq.split('=');
-      res[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1]);
-    }
-    return res;
-  }, {});
-  return {path: path, query: query};
-}
-function loadHash() {
-  var loc = parseLocation(location.hash.slice(1));
-  var route = routes[loc.path];
-  if (route) {
-    hashData.type = route.key;
-    hashData.params = loc.query;
-    if (route.init) {
-      route.init();
-      route.init = null;
-    }
-  } else {
-    location.hash = '';
-  }
-}
 function initCustomCSS() {
-  var style;
-  _.options.hook(function (changes) {
-    var customCSS = changes.customCSS || '';
+  let style;
+  options.hook((changes) => {
+    const customCSS = changes.customCSS || '';
     if (customCSS && !style) {
       style = document.createElement('style');
       document.head.appendChild(style);
     }
     if (customCSS || style) {
-      style.innerHTML = customCSS;
+      style.textContent = customCSS;
     }
   });
 }
-
-var _ = require('../common');
-_.initOptions();
-var utils = require('./utils');
-var Main = require('./views/main');
-var Confirm = require('./views/confirm');
-
-var store = Object.assign(utils.store, {
-  loading: false,
-  cache: {},
-  scripts: [],
-  sync: [],
-});
-var routes = {
-  '': {
-    key: 'Main',
-    init: initMain,
-  },
-  confirm: {
-    key: 'Confirm',
-  },
-};
-var hashData = {
-  type: null,
-  params: null,
-};
-var handlers = {
-  UpdateOptions: function (data) {
-    _.options.update(data);
-  },
-};
-browser.runtime.onMessage.addListener(function (res) {
-  var handle = handlers[res.cmd];
-  handle && handle(res.data);
-});
-window.addEventListener('hashchange', loadHash, false);
-zip.workerScriptsPath = '/public/lib/zip.js/';
-document.title = _.i18n('extName');
-loadHash();
-initCustomCSS();
-
-_.options.ready.then(function () {
-  new Vue({
-    el: '#app',
-    template: '<component :is=type :params=params></component>',
-    components: {
-      Main: Main,
-      Confirm: Confirm,
-    },
-    data: hashData,
-  });
-});

+ 22 - 0
src/options/style.css

@@ -399,3 +399,25 @@ svg path {
     height: 10em;
   }
 }
+
+.CodeMirror-foldmarker {
+	color: blue;
+	text-shadow: #b9f 1px 1px 2px, #b9f -1px -1px 2px, #b9f 1px -1px 2px, #b9f -1px 1px 2px;
+	font-family: arial;
+	line-height: .3;
+	cursor: pointer;
+}
+.CodeMirror-foldgutter {
+	width: .7em;
+}
+.CodeMirror-foldgutter-open,
+.CodeMirror-foldgutter-folded {
+	color: #555;
+	cursor: pointer;
+}
+.CodeMirror-foldgutter-open:after {
+	content: "\25BE";
+}
+.CodeMirror-foldgutter-folded:after {
+	content: "\25B8";
+}

+ 11 - 8
src/options/utils/dropdown.js

@@ -1,22 +1,25 @@
+import Vue from 'vue';
+
 Vue.directive('dropdown', {
-  bind: function (el) {
+  bind(el) {
+    const toggle = el.querySelector('[dropdown-toggle]');
+    let isOpen = false;
+    toggle.addEventListener('click', onToggle, false);
+    el.classList.add('dropdown');
     function onClose(e) {
       if (e && el.contains(e.target)) return;
       isOpen = false;
       el.classList.remove('open');
       document.removeEventListener('mousedown', onClose, false);
     }
-    function onOpen(_e) {
+    function onOpen() {
       isOpen = true;
       el.classList.add('open');
       document.addEventListener('mousedown', onClose, false);
     }
-    function onToggle(_e) {
-      isOpen ? onClose() : onOpen();
+    function onToggle() {
+      if (isOpen) onClose();
+      else onOpen();
     }
-    var toggle = el.querySelector('[dropdown-toggle]');
-    var isOpen = false;
-    toggle.addEventListener('click', onToggle, false);
-    el.classList.add('dropdown');
   },
 });

+ 37 - 31
src/options/utils/features.js

@@ -1,35 +1,39 @@
-var _ = require('src/common');
+import Vue from 'vue';
+import { initHooks } from 'src/common';
+import options from 'src/common/options';
 
-var key = 'features';
-var hooks = _.initHooks();
-var revoke = _.options.hook(function (data) {
-  if (data[key]) {
-    features = data[key];
+const FEATURES = 'features';
+let features = options.get(FEATURES);
+let hooks = initHooks();
+let revoke = options.hook((data) => {
+  if (data[FEATURES]) {
+    features = data[FEATURES];
     revoke();
     revoke = null;
     hooks.fire();
     hooks = null;
   }
 });
-var features = _.options.get(key);
-if (!features || !features.data) features = {
-  data: {},
-};
-var items = {};
+if (!features || !features.data) {
+  features = {
+    data: {},
+  };
+}
+const items = {};
 
-exports.reset = function (version) {
+export default function resetFeatures(version) {
   if (features.version !== version) {
-    _.options.set(key, features = {
-      version: version,
+    options.set(FEATURES, features = {
+      version,
       data: {},
     });
   }
-};
+}
 
 function getContext(el, value) {
-  function onFeatureClick(_e) {
+  function onFeatureClick() {
     features.data[value] = 1;
-    _.options.set(key, features);
+    options.set(FEATURES, features);
     el.classList.remove('feature');
     el.removeEventListener('click', onFeatureClick, false);
   }
@@ -43,28 +47,30 @@ function getContext(el, value) {
     el.addEventListener('click', onFeatureClick, false);
   }
   return {
-    el: el,
-    reset: reset,
-    clear: clear,
+    el,
+    clear,
+    reset,
   };
 }
 
 Vue.directive('feature', {
-  bind: function (el, binding) {
-    var value = binding.value;
-    var item = getContext(el, value);
-    var list = items[value] = items[value] || [];
+  bind(el, binding) {
+    const { value } = binding;
+    const item = getContext(el, value);
+    let list = items[value];
+    if (!list) {
+      list = [];
+      items[value] = list;
+    }
     list.push(item);
     item.reset();
-    hooks && hooks.hook(item.reset);
+    if (hooks) hooks.hook(item.reset);
   },
-  unbind: function (el, binding) {
-    var list = items[binding.value];
+  unbind(el, binding) {
+    const list = items[binding.value];
     if (list) {
-      var index = list.findIndex(function (item) {
-        return item.el === el;
-      });
-      if (~index) {
+      const index = list.findIndex(item => item.el === el);
+      if (index >= 0) {
         list[index].clear();
         list.splice(index, 1);
       }

+ 28 - 4
src/options/utils/index.js

@@ -1,5 +1,29 @@
-exports.store = {};
-exports.features = require('./features');
+import Vue from 'vue';
+import './dropdown';
+import './settings';
+import resetFeatures from './features';
+import Message from '../views/message';
 
-require('./dropdown');
-require('./settings');
+export const store = {
+  messages: null,
+};
+export const features = { reset: resetFeatures };
+
+function initMessage() {
+  if (store.messages) return;
+  store.messages = [];
+  const el = document.createElement('div');
+  document.body.appendChild(el);
+  new Vue({
+    render: h => h(Message),
+  }).$mount(el);
+}
+
+export function showMessage(data) {
+  initMessage();
+  store.messages.push(data);
+  setTimeout(() => {
+    const i = store.messages.indexOf(data);
+    if (i >= 0) store.messages.splice(i, 1);
+  }, 2000);
+}

+ 22 - 19
src/options/utils/settings.js

@@ -1,34 +1,37 @@
-var _ = require('src/common');
+import Vue from 'vue';
+import options from 'src/common/options';
 
-var hooks = {};
-_.options.hook(function (data) {
-  Object.keys(data).forEach(function (key) {
-    var list = hooks[key];
-    list && list.forEach(function (el) {
-      el.checked = data[key];
-    });
+const hooks = {};
+options.hook((data) => {
+  Object.keys(data).forEach((key) => {
+    const list = hooks[key];
+    if (list) list.forEach((el) => { el.checked = data[key]; });
   });
 });
 
 function onSettingChange(e) {
-  var target = e.target;
-  _.options.set(target.dataset.setting, target.checked);
+  const { target } = e;
+  options.set(target.dataset.setting, target.checked);
 }
 
 Vue.directive('setting', {
-  bind: function (el, binding) {
-    var value = binding.value;
+  bind(el, binding) {
+    const { value } = binding;
     el.dataset.setting = value;
     el.addEventListener('change', onSettingChange, false);
-    var list = hooks[value] = hooks[value] || [];
+    let list = hooks[value];
+    if (!list) {
+      list = [];
+      hooks[value] = list;
+    }
     list.push(el);
-    el.checked = _.options.get(value);
+    el.checked = options.get(value);
   },
-  unbind: function (el, binding) {
-    var value = binding.value;
+  unbind(el, binding) {
+    const { value } = binding;
     el.removeEventListener('change', onSettingChange, false);
-    var list = hooks[value] || [];
-    var i = list.indexOf(el);
-    ~i && list.splice(i, 1);
+    const list = hooks[value] || [];
+    const i = list.indexOf(el);
+    if (i >= 0) list.splice(i, 1);
   },
 });

+ 99 - 0
src/options/views/code.vue

@@ -0,0 +1,99 @@
+<template>
+  <div class="editor-code"></div>
+</template>
+
+<script>
+import 'codemirror/lib/codemirror.css';
+import 'codemirror/theme/eclipse.css';
+import 'codemirror/mode/javascript/javascript';
+import 'codemirror/addon/comment/continuecomment';
+import 'codemirror/addon/edit/matchbrackets';
+import 'codemirror/addon/edit/closebrackets';
+import 'codemirror/addon/fold/foldcode';
+import 'codemirror/addon/fold/foldgutter';
+import 'codemirror/addon/fold/brace-fold';
+import 'codemirror/addon/fold/comment-fold';
+import 'codemirror/addon/search/match-highlighter';
+import 'codemirror/addon/search/searchcursor';
+import 'codemirror/addon/selection/active-line';
+import CodeMirror from 'codemirror';
+
+function getHandler(key) {
+  return (cm) => {
+    const { commands } = cm.state;
+    const handle = commands && commands[key];
+    return handle && handle();
+  };
+}
+function indentWithTab(cm) {
+  if (cm.somethingSelected()) {
+    cm.indentSelection('add');
+  } else {
+    cm.replaceSelection(
+      cm.getOption('indentWithTabs') ? '\t' : ' '.repeat(cm.getOption('indentUnit')),
+      'end', '+input');
+  }
+}
+
+[
+  'save', 'cancel', 'find', 'findNext', 'findPrev', 'replace', 'replaceAll',
+].forEach((key) => {
+  CodeMirror.commands[key] = getHandler(key);
+});
+
+export default {
+  props: [
+    'readonly',
+    'content',
+    'commands',
+  ],
+  mounted() {
+    const cm = CodeMirror(this.$el, {
+      continueComments: true,
+      matchBrackets: true,
+      autoCloseBrackets: true,
+      highlightSelectionMatches: true,
+      lineNumbers: true,
+      mode: 'javascript',
+      lineWrapping: true,
+      styleActiveLine: true,
+      foldGutter: true,
+      gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
+      theme: 'eclipse',
+    });
+    this.cm = cm;
+    if (this.readonly) cm.setOption('readOnly', true);
+    cm.on('change', () => {
+      this.cachedContent = cm.getValue();
+      this.$emit('change', this.cachedContent);
+    });
+    cm.state.commands = this.commands;
+    cm.setOption('extraKeys', {
+      Esc: 'cancel',
+      Tab: indentWithTab,
+    });
+    cm.on('keyHandled', (_cm, _name, e) => {
+      e.stopPropagation();
+    });
+    this.update();
+    this.$emit('ready', cm);
+  },
+  watch: {
+    content(content) {
+      if (content !== this.cachedContent) {
+        this.cachedContent = content;
+        this.update();
+      }
+    },
+  },
+  methods: {
+    update() {
+      const { cm } = this;
+      if (!cm || this.cachedContent == null) return;
+      cm.setValue(this.cachedContent);
+      cm.getDoc().clearHistory();
+      cm.focus();
+    },
+  },
+};
+</script>

+ 0 - 28
src/options/views/confirm.html

@@ -1,28 +0,0 @@
-<div class="flex flex-col h-100">
-  <div class="frame-block">
-    <div class="buttons pull-right">
-      <div v-dropdown>
-        <button dropdown-toggle v-text="i18n('buttonInstallOptions')"></button>
-        <div class="dropdown-menu options-panel" @mousedown.stop>
-          <label>
-            <input type=checkbox v-setting="'closeAfterInstall'" @change="checkClose">
-            <span v-text="i18n('installOptionClose')"></span>
-          </label>
-          <label>
-            <input type=checkbox v-setting="'trackLocalFile'" :disabled="options.closeAfterInstall">
-            <span v-text="i18n('installOptionTrack')"></span>
-          </label>
-        </div>
-      </div>
-      <button v-text="i18n('buttonConfirmInstallation')"
-        :disabled="!installable" @click="installScript"></button>
-      <button v-text="i18n('buttonClose')" @click="close"></button>
-    </div>
-    <h1><span v-text="i18n('labelInstall')"></span> - <span v-text="i18n('extName')"></span></h1>
-    <div class="ellipsis confirm-url" :title="params.url" v-text="params.url"></div>
-    <div class="ellipsis confirm-msg" v-text="message"></div>
-  </div>
-  <div class="frame-block flex-auto p-rel">
-    <editor class="abs-full" readonly :content="code" :commands="commands"></editor>
-  </div>
-</div>

+ 0 - 194
src/options/views/confirm.js

@@ -1,194 +0,0 @@
-var Editor = require('./editor');
-var cache = require('../../cache');
-var _ = require('../../common');
-
-var options = {
-  closeAfterInstall: _.options.get('closeAfterInstall'),
-};
-
-_.options.hook(function (changes) {
-  if ('closeAfterInstall' in changes) {
-    options.closeAfterInstall = changes.closeAfterInstall;
-  }
-});
-
-module.exports = {
-  props: ['params'],
-  components: {
-    Editor: Editor,
-  },
-  template: cache.get('./confirm.html'),
-  data: function () {
-    return {
-      installable: false,
-      dependencyOK: false,
-      message: '',
-      code: '',
-      require: {},
-      resources: {},
-      options: options,
-      commands: {
-        cancel: this.close,
-      },
-    };
-  },
-  computed: {
-    isLocal: function () {
-      return /^file:\/\/\//.test(this.params.u);
-    },
-  },
-  mounted: function () {
-    var _this = this;
-    _this.message = _.i18n('msgLoadingData');
-    _this.loadData().then(function () {
-      _this.parseMeta();
-    });
-  },
-  methods: {
-    loadData: function (changedOnly) {
-      var _this = this;
-      _this.installable = false;
-      var oldCode = _this.code;
-      return _this.getScript(_this.params.u)
-      .then(function (code) {
-        if (changedOnly && oldCode === code) return Promise.reject();
-        _this.code = code;
-      });
-    },
-    parseMeta: function () {
-      var _this = this;
-      return _.sendMessage({
-        cmd: 'ParseMeta',
-        data: _this.code,
-      })
-      .then(function (script) {
-        var urls = Object.keys(script.resources).map(function (key) {
-          return script.resources[key];
-        });
-        var length = script.require.length + urls.length;
-        if (!length) return;
-        var finished = 0;
-        var error = [];
-        var updateStatus = function () {
-          _this.message = _.i18n('msgLoadingDependency', [finished, length]);
-        };
-        updateStatus();
-        var promises = script.require.map(function (url) {
-          return _this.getFile(url).then(function (res) {
-            _this.require[url] = res;
-          });
-        });
-        promises = promises.concat(urls.map(function (url) {
-          return _this.getFile(url, true).then(function (res) {
-            _this.resources[url] = res;
-          });
-        }));
-        promises = promises.map(function (promise) {
-          return promise.then(function () {
-            finished += 1;
-            updateStatus();
-          }, function (url) {
-            error.push(url);
-          });
-        });
-        return Promise.all(promises).then(function () {
-          if (error.length) return Promise.reject(error.join('\n'));
-          _this.dependencyOK = true;
-        });
-      })
-      .then(function () {
-        _this.message = _.i18n('msgLoadedData');
-        _this.installable = true;
-      }, function (err) {
-        _this.message = _.i18n('msgErrorLoadingDependency', [err]);
-        return Promise.reject();
-      });
-    },
-    close: function () {
-      window.close();
-    },
-    getFile: function (url, isBlob) {
-      return new Promise(function (resolve, reject) {
-        var xhr = new XMLHttpRequest;
-        xhr.open('GET', url, true);
-        if (isBlob) xhr.responseType = 'blob';
-        xhr.onloadend = function () {
-          if (xhr.status > 300) return reject(url);
-          if (isBlob) {
-            var reader = new FileReader;
-            reader.onload = function () {
-              resolve(window.btoa(this.result));
-            };
-            reader.readAsBinaryString(xhr.response);
-          } else {
-            resolve(xhr.responseText);
-          }
-        };
-        xhr.send();
-      });
-    },
-    getScript: function (url) {
-      var _this = this;
-      return _.sendMessage({
-        cmd: 'GetFromCache',
-        data: url,
-      })
-      .then(function (text) {
-        return text || Promise.reject();
-      })
-      .catch(function () {
-        return _this.getFile(url);
-      })
-      .catch(function (url) {
-        _this.message = _.i18n('msgErrorLoadingData');
-        throw url;
-      });
-    },
-    getTimeString: function () {
-      var now = new Date;
-      return _.zfill(now.getHours(), 2) + ':' +
-        _.zfill(now.getMinutes(), 2) + ':' +
-        _.zfill(now.getSeconds(), 2);
-    },
-    installScript: function () {
-      var _this = this;
-      _this.installable = false;
-      _.sendMessage({
-        cmd:'ParseScript',
-        data:{
-          url: _this.params.u,
-          from: _this.params.f,
-          code: _this.code,
-          require: _this.require,
-          resources: _this.resources,
-        },
-      })
-      .then(function (res) {
-        _this.message = res.message + '[' + _this.getTimeString() + ']';
-        if (res.code < 0) return;
-        if (_this.closeAfterInstall) _this.close();
-        else if (_this.isLocal && _.options.get('trackLocalFile')) _this.trackLocalFile();
-      });
-    },
-    trackLocalFile: function () {
-      var _this = this;
-      new Promise(function (resolve) {
-        setTimeout(resolve, 2000);
-      })
-      .then(function () {
-        return _this.loadData(true).then(function () {
-          return _this.parseMeta();
-        });
-      })
-      .then(function () {
-        var track = _.options.get('trackLocalFile');
-        track && _this.installScript();
-      }, function () {
-        _this.trackLocalFile();
-      });
-    },
-    checkClose: function (e) {
-      e.target.checked && _.options.set('trackLocalFile', false);
-    },
-  },
-};

+ 202 - 0
src/options/views/confirm.vue

@@ -0,0 +1,202 @@
+<template>
+  <div class="flex flex-col h-100">
+    <div class="frame-block">
+      <div class="buttons pull-right">
+        <div v-dropdown>
+          <button dropdown-toggle v-text="i18n('buttonInstallOptions')"></button>
+          <div class="dropdown-menu options-panel" @mousedown.stop>
+            <label>
+              <input type=checkbox v-setting="'closeAfterInstall'" @change="checkClose">
+              <span v-text="i18n('installOptionClose')"></span>
+            </label>
+            <label>
+              <input type=checkbox v-setting="'trackLocalFile'" :disabled="settings.closeAfterInstall">
+              <span v-text="i18n('installOptionTrack')"></span>
+            </label>
+          </div>
+        </div>
+        <button v-text="i18n('buttonConfirmInstallation')"
+        :disabled="!installable" @click="installScript"></button>
+        <button v-text="i18n('buttonClose')" @click="close"></button>
+      </div>
+      <h1><span v-text="i18n('labelInstall')"></span> - <span v-text="i18n('extName')"></span></h1>
+      <div class="ellipsis confirm-url" :title="params.url" v-text="params.url"></div>
+      <div class="ellipsis confirm-msg" v-text="message"></div>
+    </div>
+    <div class="frame-block flex-auto p-rel">
+      <Code class="abs-full" readonly :content="code" :commands="commands" />
+    </div>
+  </div>
+</template>
+
+<script>
+import { sendMessage, zfill } from 'src/common';
+import options from 'src/common/options';
+import Code from './code';
+
+const settings = {
+  closeAfterInstall: options.get('closeAfterInstall'),
+};
+
+options.hook((changes) => {
+  if ('closeAfterInstall' in changes) {
+    settings.closeAfterInstall = changes.closeAfterInstall;
+  }
+});
+
+export default {
+  props: ['params'],
+  components: {
+    Code,
+  },
+  data() {
+    return {
+      settings,
+      installable: false,
+      dependencyOK: false,
+      message: '',
+      code: '',
+      require: {},
+      resources: {},
+      commands: {
+        cancel: this.close,
+      },
+    };
+  },
+  computed: {
+    isLocal() {
+      return /^file:\/\/\//.test(this.params.u);
+    },
+  },
+  mounted() {
+    this.message = this.i18n('msgLoadingData');
+    this.loadData().then(this.parseMeta);
+  },
+  methods: {
+    loadData(changedOnly) {
+      this.installable = false;
+      const { code: oldCode } = this;
+      return this.getScript(this.params.u)
+      .then((code) => {
+        if (changedOnly && oldCode === code) return Promise.reject();
+        this.code = code;
+      });
+    },
+    parseMeta() {
+      return sendMessage({
+        cmd: 'ParseMeta',
+        data: this.code,
+      })
+      .then((script) => {
+        const urls = Object.keys(script.resources)
+        .map(key => script.resources[key]);
+        const length = script.require.length + urls.length;
+        if (!length) return;
+        let finished = 0;
+        let error = [];
+        const updateStatus = () => {
+          this.message = this.i18n('msgLoadingDependency', [finished, length]);
+        };
+        updateStatus();
+        let promises = script.require.map(url => this.getFile(url).then((res) => {
+          this.require[url] = res;
+        }))
+        .concat(urls.map(url => this.getFile(url, true).then((res) => {
+          this.resources[url] = res;
+        })));
+        promises = promises.map(promise => promise.then(() => {
+          finished += 1;
+          updateStatus();
+        }, (url) => {
+          error.push(url);
+        }));
+        return Promise.all(promises).then(() => {
+          if (error.length) return Promise.reject(error.join('\n'));
+          this.dependencyOK = true;
+        });
+      })
+      .then(() => {
+        this.message = this.i18n('msgLoadedData');
+        this.installable = true;
+      }, (err) => {
+        this.message = this.i18n('msgErrorLoadingDependency', [err]);
+        return Promise.reject();
+      });
+    },
+    close() {
+      window.close();
+    },
+    getFile(url, isBlob) {
+      return new Promise((resolve, reject) {
+        const xhr = new XMLHttpRequest;
+        xhr.open('GET', url, true);
+        if (isBlob) xhr.responseType = 'blob';
+        xhr.onloadend = () => {
+          if (xhr.status > 300) return reject(url);
+          if (isBlob) {
+            const reader = new FileReader();
+            reader.onload = function onload() {
+              resolve(window.btoa(this.result));
+            };
+            reader.readAsBinaryString(xhr.response);
+          } else {
+            resolve(xhr.responseText);
+          }
+        };
+        xhr.send();
+      });
+    },
+    getScript(url) {
+      return sendMessage({
+        cmd: 'GetFromCache',
+        data: url,
+      })
+      .then(text => text || Promise.reject())
+      .catch(() => this.getFile(url))
+      .catch((url) => {
+        this.message = this.i18n('msgErrorLoadingData');
+        throw url;
+      });
+    },
+    getTimeString() {
+      const now = new Date();
+      return `${zfill(now.getHours(), 2)}:${zfill(now.getMinutes(), 2)}:${zfill(now.getSeconds(), 2)}`;
+    },
+    installScript() {
+      this.installable = false;
+      sendMessage({
+        cmd: 'ParseScript',
+        data: {
+          url: this.params.u,
+          from: this.params.f,
+          code: this.code,
+          require: this.require,
+          resources: this.resources,
+        },
+      })
+      .then((res) => {
+        this.message = `${res.message}[${this.getTimeString()}]`;
+        if (res.code < 0) return;
+        if (this.closeAfterInstall) this.close();
+        else if (this.isLocal && options.get('trackLocalFile')) this.trackLocalFile();
+      });
+    },
+    trackLocalFile() {
+      new Promise((resolve) => {
+        setTimeout(resolve, 2000);
+      })
+      .then(() => this.loadData(true))
+      .then(this.parseMeta)
+      .then(() => {
+        const track = options.get('trackLocalFile');
+        if (track) this.installScript();
+      }, () => {
+        this.trackLocalFile();
+      });
+    },
+    checkClose(e) {
+      if (e.target.checked) options.set('trackLocalFile', false);
+    },
+  },
+};
+</script>

+ 0 - 120
src/options/views/edit.html

@@ -1,120 +0,0 @@
-<div class="edit flex flex-col fixed-full">
-  <div class="frame-block">
-    <div class="buttons pull-right">
-      <a class="mr-1" href="https://violentmonkey.github.io/2017/03/14/How-to-edit-scripts-with-your-favorite-editor/" target="_blank">How to edit locally?</a>
-      <div v-dropdown>
-        <button dropdown-toggle v-text="i18n('buttonCustomMeta')"></button>
-        <div class="dropdown-menu">
-          <table>
-            <tr>
-              <td title="@name" v-text="i18n('labelName')"></td>
-              <td class=expand>
-                <input type="text" v-model="custom.name" :placeholder="placeholders.name">
-              </td>
-              <td title="@run-at" v-text="i18n('labelRunAt')"></td>
-              <td>
-                <select v-model="custom['run-at']">
-                  <option value="" v-text="i18n('labelRunAtDefault')"></option>
-                  <option value=start>document-start</option>
-                  <option value=idle>document-idle</option>
-                  <option value=end>document-end</option>
-                </select>
-              </td>
-            </tr>
-            <tr title="@homepageURL">
-              <td v-text="i18n('labelHomepageURL')"></td>
-              <td colspan=3 class=expand>
-                <input type="text" v-model="custom.homepageURL" :placeholder="placeholders.homepageURL">
-              </td>
-            </tr>
-          </table>
-          <table>
-            <tr title="@updateURL">
-              <td v-text="i18n('labelUpdateURL')"></td>
-              <td class=expand>
-                <input type="text" v-model="custom.updateURL" :placeholder="placeholders.updateURL">
-              </td>
-            </tr>
-            <tr title="@downloadURL">
-              <td v-text="i18n('labelDownloadURL')"></td>
-              <td class=expand>
-                <input type="text" v-model="custom.downloadURL" :placeholder="placeholders.downloadURL">
-              </td>
-            </tr>
-          </table>
-          <fieldset title="@include">
-            <legend>
-              <span v-text="i18n('labelInclude')"></span>
-              <label>
-                <input type=checkbox v-model="custom.keepInclude">
-                <span v-text="i18n('labelKeepInclude')"></span>
-              </label>
-            </legend>
-            <div v-html="i18n('labelCustomInclude')"></div>
-            <textarea v-model="custom.include"></textarea>
-          </fieldset>
-          <fieldset title="@match">
-            <legend>
-              <span v-text="i18n('labelMatch')"></span>
-              <label>
-                <input type=checkbox v-model="custom.keepMatch">
-                <span v-text="i18n('labelKeepMatch')"></span>
-              </label>
-            </legend>
-            <div v-html="i18n('labelCustomMatch')"></div>
-            <textarea v-model="custom.match"></textarea>
-          </fieldset>
-          <fieldset title="@exclude">
-            <legend>
-              <span v-text="i18n('labelExclude')"></span>
-              <label>
-                <input type=checkbox v-model="custom.keepExclude">
-                <span v-text="i18n('labelKeepExclude')"></span>
-              </label>
-            </legend>
-            <div v-html="i18n('labelCustomExclude')"></div>
-            <textarea v-model="custom.exclude"></textarea>
-          </fieldset>
-        </div>
-      </div>
-    </div>
-    <h2 v-text="i18n('labelScriptEditor')"></h2>
-  </div>
-  <div class="frame-block flex-auto p-rel">
-    <editor
-      class="abs-full"
-      :content="code" :commands="commands"
-      @change="contentChange" @ready="initEditor"
-    />
-  </div>
-  <div class="frame-block" v-show="search.show">
-    <button class="pull-right" @click="clearSearch">&times;</button>
-    <form class="inline-block mr-1" @submit.prevent="goToLine()">
-      <span v-text="i18n('labelLineNumber')"></span>
-      <input class="w-1" v-model="search.line">
-    </form>
-    <form class="inline-block mr-1" @submit.prevent="findNext()">
-      <span v-text="i18n('labelSearch')"></span>
-      <input ref="search" v-model="search.state.query" title="Ctrl-F">
-      <button type="button" @click="findNext(1)" title="Shift-Ctrl-G">&lt;</button>
-      <button type="submit" title="Ctrl-G">&gt;</button>
-    </form>
-    <form class="inline-block mr-1" @submit.prevent="replace()">
-      <span v-text="i18n('labelReplace')"></span>
-      <input v-model="search.state.replace">
-      <button type="submit" v-text="i18n('buttonReplace')" title="Shift-Ctrl-F"></button>
-      <button type="button" v-text="i18n('buttonReplaceAll')" @click="replace(1)" title="Shift-Ctrl-R"></button>
-    </form>
-  </div>
-  <div class="frame-block">
-    <div class="pull-right">
-      <button v-text="i18n('buttonSave')" @click="save" :disabled="!canSave"></button>
-      <button v-text="i18n('buttonSaveClose')" @click="saveClose" :disabled="!canSave"></button>
-      <button v-text="i18n('buttonClose')" @click="close"></button>
-    </div>
-    <label>
-      <input type=checkbox v-model="update">
-      <span v-text="i18n('labelAllowUpdate')"></span>
-    </label>
-  </div>
-</div>

+ 0 - 300
src/options/views/edit.js

@@ -1,300 +0,0 @@
-function fromList(list) {
-  return (list || []).join('\n');
-}
-function toList(text) {
-  return text.split('\n')
-  .map(function (line) {
-    return line.trim();
-  })
-  .filter(function (item) {
-    return item;
-  });
-}
-function findNext(cm, state, reversed) {
-  cm.operation(function () {
-    var query = state.query || '';
-    var cursor = cm.getSearchCursor(query, reversed ? state.posFrom : state.posTo);
-    if (!cursor.find(reversed)) {
-      cursor = cm.getSearchCursor(query, reversed ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0));
-      if (!cursor.find(reversed)) return;
-    }
-    cm.setSelection(cursor.from(), cursor.to());
-    state.posFrom = cursor.from();
-    state.posTo = cursor.to();
-  });
-}
-function replaceOne(cm, state) {
-  var start = cm.getCursor('start');
-  var end = cm.getCursor('end');
-  state.posTo = state.posFrom;
-  findNext(cm, state);
-  var start_ = cm.getCursor('start');
-  var end_ = cm.getCursor('end');
-  if (
-    start.line === start_.line && start.ch === start_.ch
-    && end.line === end_.line && end.ch === end_.ch
-  ) {
-    cm.replaceRange(state.replace, start, end);
-    findNext(cm, state);
-  }
-}
-function replaceAll(cm, state) {
-  cm.operation(function () {
-    var query = state.query || '';
-    for (var cursor = cm.getSearchCursor(query); cursor.findNext();) {
-      cursor.replace(state.replace);
-    }
-  });
-}
-
-var Message = require('./message');
-var Editor = require('./editor');
-var cache = require('../../cache');
-var _ = require('../../common');
-
-module.exports = {
-  props: ['script'],
-  template: cache.get('./edit.html'),
-  components: {
-    Editor: Editor,
-  },
-  data: function () {
-    var _this = this;
-    _this.debouncedFind = _.debounce(_this.find, 100);
-    return {
-      canSave: false,
-      update: false,
-      code: '',
-      custom: {},
-      search: {
-        show: false,
-        state: {
-          query: null,
-          replace: null,
-        },
-      },
-      commands: {
-        save: _this.save,
-        cancel: function () {
-          if (_this.search.show) {
-            _this.clearSearch();
-          } else {
-            _this.close();
-          }
-        },
-        find: _this.find,
-        findNext: _this.findNext,
-        findPrev: function () {
-          _this.findNext(1);
-        },
-        replace: _this.replace,
-        replaceAll: function () {
-          _this.replace(1);
-        },
-      },
-    };
-  },
-  computed: {
-    placeholders: function () {
-      var script = this.script;
-      return {
-        name: script.meta.name,
-        homepageURL: script.meta.homepageURL,
-        updateURL: script.meta.updateURL || _.i18n('hintUseDownloadURL'),
-        downloadURL: script.meta.downloadURL || script.lastInstallURL,
-      };
-    },
-  },
-  watch: {
-    custom: {
-      deep: true,
-      handler: function () {
-        this.canSave = true;
-      },
-    },
-    'search.state.query': function (query) {
-      query && this.debouncedFind();
-    },
-  },
-  mounted: function () {
-    var _this = this;
-    (_this.script.id ? _.sendMessage({
-      cmd: 'GetScript',
-      data: _this.script.id,
-    }) : Promise.resolve(_this.script))
-    .then(function (script) {
-      _this.update = script.update;
-      _this.code = script.code;
-      var custom = script.custom;
-      _this.custom = [
-        'name',
-        'homepageURL',
-        'updateURL',
-        'downloadURL',
-      ].reduce(function (value, key) {
-        value[key] = custom[key];
-        return value;
-      }, {
-        keepInclude: custom._include !== false,
-        keepMatch: custom._match !== false,
-        keepExclude: custom._exclude !== false,
-        include: fromList(custom.include),
-        match: fromList(custom.match),
-        exclude: fromList(custom.exclude),
-        'run-at': custom['run-at'] || '',
-      });
-      _this.$nextTick(function () {
-        _this.canSave = false;
-      });
-    });
-  },
-  beforeDestroy: function () {
-    var _this = this;
-    _this.cm && _this.unbindKeys();
-  },
-  methods: {
-    save: function () {
-      var _this = this;
-      var custom = _this.custom;
-      var value = [
-        'name',
-        'run-at',
-        'homepageURL',
-        'updateURL',
-        'downloadURL',
-      ].reduce(function (value, key) {
-        value[key] = custom[key];
-        return value;
-      }, {
-        _include: custom.keepInclude,
-        _match: custom.keepMatch,
-        _exclude: custom.keepExclude,
-        include: toList(custom.include),
-        match: toList(custom.match),
-        exclude: toList(custom.exclude),
-      });
-      return _.sendMessage({
-        cmd: 'ParseScript',
-        data: {
-          id: _this.script.id,
-          code: _this.code,
-          // User created scripts MUST be marked `isNew` so that
-          // the backend is able to check namespace conflicts
-          isNew: !_this.script.id,
-          message: '',
-          more: {
-            custom: value,
-            update: _this.update,
-          },
-        },
-      })
-      .then(function (script) {
-        _this.script = script;
-        _this.canSave = false;
-      }, function (err) {
-        Message.open({text: err});
-      });
-    },
-    close: function () {
-      var _this = this;
-      if (!_this.canSave || confirm(_.i18n('confirmNotSaved'))) {
-        _this.$emit('close');
-      }
-    },
-    saveClose: function () {
-      var _this = this;
-      _this.save().then(function () {
-        _this.close();
-      });
-    },
-    contentChange: function (code) {
-      var _this = this;
-      _this.code = code;
-      _this.canSave = true;
-    },
-    initEditor: function (cm) {
-      var _this = this;
-      _this.cm = cm;
-      _this.bindKeys();
-    },
-    find: function () {
-      var _this = this;
-      var state = _this.search.state;
-      state.posTo = state.posFrom;
-      _this.findNext();
-    },
-    findNext: function (reversed) {
-      var _this = this;
-      var state = _this.search.state;
-      var cm = _this.cm;
-      if (state.query) {
-        findNext(cm, state, reversed);
-      }
-      _this.search.show = true;
-      _this.$nextTick(function () {
-        _this.$refs.search.focus();
-      });
-    },
-    clearSearch: function () {
-      var _this = this;
-      var cm = _this.cm;
-      cm.operation(function () {
-        var state = _this.search.state;
-        state.posFrom = state.posTo = null;
-        _this.search.show = false;
-      });
-      cm.focus();
-    },
-    replace: function (all) {
-      var _this = this;
-      var cm = _this.cm;
-      var state = _this.search.state;
-      if (!state.query) {
-        _this.find();
-        return;
-      }
-      (all ? replaceAll : replaceOne)(cm, state);
-    },
-    onKeyDown: function (e) {
-      var _this = this;
-      var cm = _this.cm;
-      var name = CodeMirror.keyName(e);
-      var commands = [
-        'cancel',
-        'find',
-        'findNext',
-        'findPrev',
-        'replace',
-        'replaceAll',
-      ];
-      [
-        cm.options.extraKeys,
-        cm.options.keyMap,
-      ].some(function (keyMap) {
-        var stop = false;
-        keyMap && CodeMirror.lookupKey(name, keyMap, function (b) {
-          if (~commands.indexOf(b)) {
-            e.preventDefault();
-            e.stopPropagation();
-            cm.execCommand(b);
-            stop = true;
-          }
-        }, cm);
-        return stop;
-      });
-    },
-    bindKeys: function () {
-      window.addEventListener('keydown', this.onKeyDown, false);
-    },
-    unbindKeys: function () {
-      window.removeEventListener('keydown', this.onKeyDown, false);
-    },
-    goToLine: function () {
-      var _this = this;
-      var line = _this.search.line - 1;
-      var cm = _this.cm;
-      !isNaN(line) && cm.setCursor(line, 0);
-      cm.focus();
-    },
-  },
-};

+ 408 - 0
src/options/views/edit.vue

@@ -0,0 +1,408 @@
+<template>
+  <div class="edit flex flex-col fixed-full">
+    <div class="frame-block">
+      <div class="buttons pull-right">
+        <a class="mr-1" href="https://violentmonkey.github.io/2017/03/14/How-to-edit-scripts-with-your-favorite-editor/" target="_blank">How to edit locally?</a>
+        <div v-dropdown>
+          <button dropdown-toggle v-text="i18n('buttonCustomMeta')"></button>
+          <div class="dropdown-menu">
+            <table>
+              <tr>
+                <td title="@name" v-text="i18n('labelName')"></td>
+                <td class="expand">
+                  <input type="text" v-model="custom.name" :placeholder="placeholders.name">
+                </td>
+                <td title="@run-at" v-text="i18n('labelRunAt')"></td>
+                <td>
+                  <select v-model="custom['run-at']">
+                    <option value="" v-text="i18n('labelRunAtDefault')"></option>
+                    <option value=start>document-start</option>
+                    <option value=idle>document-idle</option>
+                    <option value=end>document-end</option>
+                  </select>
+                </td>
+              </tr>
+              <tr title="@homepageURL">
+                <td v-text="i18n('labelHomepageURL')"></td>
+                <td colspan=3 class=expand>
+                  <input type="text" v-model="custom.homepageURL" :placeholder="placeholders.homepageURL">
+                </td>
+              </tr>
+            </table>
+            <table>
+              <tr title="@updateURL">
+                <td v-text="i18n('labelUpdateURL')"></td>
+                <td class=expand>
+                  <input type="text" v-model="custom.updateURL" :placeholder="placeholders.updateURL">
+                </td>
+              </tr>
+              <tr title="@downloadURL">
+                <td v-text="i18n('labelDownloadURL')"></td>
+                <td class=expand>
+                  <input type="text" v-model="custom.downloadURL" :placeholder="placeholders.downloadURL">
+                </td>
+              </tr>
+            </table>
+            <fieldset title="@include">
+              <legend>
+                <span v-text="i18n('labelInclude')"></span>
+                <label>
+                  <input type=checkbox v-model="custom.keepInclude">
+                  <span v-text="i18n('labelKeepInclude')"></span>
+                </label>
+              </legend>
+              <div v-html="i18n('labelCustomInclude')"></div>
+              <textarea v-model="custom.include"></textarea>
+            </fieldset>
+            <fieldset title="@match">
+              <legend>
+                <span v-text="i18n('labelMatch')"></span>
+                <label>
+                  <input type=checkbox v-model="custom.keepMatch">
+                  <span v-text="i18n('labelKeepMatch')"></span>
+                </label>
+              </legend>
+              <div v-html="i18n('labelCustomMatch')"></div>
+              <textarea v-model="custom.match"></textarea>
+            </fieldset>
+            <fieldset title="@exclude">
+              <legend>
+                <span v-text="i18n('labelExclude')"></span>
+                <label>
+                  <input type=checkbox v-model="custom.keepExclude">
+                  <span v-text="i18n('labelKeepExclude')"></span>
+                </label>
+              </legend>
+              <div v-html="i18n('labelCustomExclude')"></div>
+              <textarea v-model="custom.exclude"></textarea>
+            </fieldset>
+          </div>
+        </div>
+      </div>
+      <h2 v-text="i18n('labelScriptEditor')"></h2>
+    </div>
+    <div class="frame-block flex-auto p-rel">
+      <Code
+      class="abs-full"
+      :content="code" :commands="commands"
+      @change="contentChange" @ready="initEditor"
+      />
+    </div>
+    <div class="frame-block" v-show="search.show">
+      <button class="pull-right" @click="clearSearch">&times;</button>
+      <form class="inline-block mr-1" @submit.prevent="goToLine()">
+        <span v-text="i18n('labelLineNumber')"></span>
+        <input class="w-1" v-model="search.line">
+      </form>
+      <form class="inline-block mr-1" @submit.prevent="findNext()">
+        <span v-text="i18n('labelSearch')"></span>
+        <input ref="search" v-model="search.state.query" title="Ctrl-F">
+        <button type="button" @click="findNext(1)" title="Shift-Ctrl-G">&lt;</button>
+        <button type="submit" title="Ctrl-G">&gt;</button>
+      </form>
+      <form class="inline-block mr-1" @submit.prevent="replace()">
+        <span v-text="i18n('labelReplace')"></span>
+        <input v-model="search.state.replace">
+        <button type="submit" v-text="i18n('buttonReplace')" title="Shift-Ctrl-F"></button>
+        <button type="button" v-text="i18n('buttonReplaceAll')" @click="replace(1)" title="Shift-Ctrl-R"></button>
+      </form>
+    </div>
+    <div class="frame-block">
+      <div class="pull-right">
+        <button v-text="i18n('buttonSave')" @click="save" :disabled="!canSave"></button>
+        <button v-text="i18n('buttonSaveClose')" @click="saveClose" :disabled="!canSave"></button>
+        <button v-text="i18n('buttonClose')" @click="close"></button>
+      </div>
+      <label>
+        <input type=checkbox v-model="update">
+        <span v-text="i18n('labelAllowUpdate')"></span>
+      </label>
+    </div>
+  </div>
+</template>
+
+<script>
+import CodeMirror from 'codemirror';
+import { i18n, debounce, sendMessage } from 'src/common';
+import { showMessage } from '../utils';
+import Code from './code';
+
+function fromList(list) {
+  return (list || []).join('\n');
+}
+function toList(text) {
+  return text.split('\n')
+  .map(line => line.trim())
+  .filter(Boolean);
+}
+function findNext(cm, state, reversed) {
+  cm.operation(() => {
+    const query = state.query || '';
+    let cursor = cm.getSearchCursor(query, reversed ? state.posFrom : state.posTo);
+    if (!cursor.find(reversed)) {
+      cursor = cm.getSearchCursor(query,
+        reversed ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0));
+      if (!cursor.find(reversed)) return;
+    }
+    cm.setSelection(cursor.from(), cursor.to());
+    state.posFrom = cursor.from();
+    state.posTo = cursor.to();
+  });
+}
+function replaceOne(cm, state) {
+  const start = cm.getCursor('start');
+  const end = cm.getCursor('end');
+  state.posTo = state.posFrom;
+  findNext(cm, state);
+  const start2 = cm.getCursor('start');
+  const end2 = cm.getCursor('end');
+  if (
+    start.line === start2.line && start.ch === start2.ch
+    && end.line === end2.line && end.ch === end2.ch
+  ) {
+    cm.replaceRange(state.replace, start, end);
+    findNext(cm, state);
+  }
+}
+function replaceAll(cm, state) {
+  cm.operation(() => {
+    const query = state.query || '';
+    for (let cursor = cm.getSearchCursor(query); cursor.findNext();) {
+      cursor.replace(state.replace);
+    }
+  });
+}
+
+export default {
+  props: ['script'],
+  components: {
+    Code,
+  },
+  data() {
+    this.debouncedFind = debounce(this.find, 100);
+    return {
+      canSave: false,
+      update: false,
+      code: '',
+      custom: {},
+      search: {
+        show: false,
+        state: {
+          query: null,
+          replace: null,
+        },
+      },
+      commands: {
+        save: this.save,
+        cancel: () => {
+          if (this.search.show) {
+            this.clearSearch();
+          } else {
+            this.close();
+          }
+        },
+        find: this.find,
+        findNext: this.findNext,
+        findPrev: () => {
+          this.findNext(1);
+        },
+        replace: this.replace,
+        replaceAll: () => {
+          this.replace(1);
+        },
+      },
+    };
+  },
+  computed: {
+    placeholders() {
+      const { script } = this;
+      return {
+        name: script.meta.name,
+        homepageURL: script.meta.homepageURL,
+        updateURL: script.meta.updateURL || i18n('hintUseDownloadURL'),
+        downloadURL: script.meta.downloadURL || script.lastInstallURL,
+      };
+    },
+  },
+  watch: {
+    custom: {
+      deep: true,
+      handler() {
+        this.canSave = true;
+      },
+    },
+    'search.state.query'() {
+      this.debouncedFind();
+    },
+  },
+  mounted() {
+    (this.script.id ? sendMessage({
+      cmd: 'GetScript',
+      data: this.script.id,
+    }) : Promise.resolve(this.script))
+    .then((script) => {
+      this.update = script.update;
+      this.code = script.code;
+      const { custom } = script;
+      this.custom = [
+        'name',
+        'homepageURL',
+        'updateURL',
+        'downloadURL',
+      ].reduce((value, key) => {
+        value[key] = custom[key];
+        return value;
+      }, {
+        keepInclude: custom._include !== false,
+        keepMatch: custom._match !== false,
+        keepExclude: custom._exclude !== false,
+        include: fromList(custom.include),
+        match: fromList(custom.match),
+        exclude: fromList(custom.exclude),
+        'run-at': custom['run-at'] || '',
+      });
+      this.$nextTick(() => {
+        this.canSave = false;
+      });
+    });
+  },
+  beforeDestroy() {
+    if (this.cm) this.unbindKeys();
+  },
+  methods: {
+    save() {
+      const { custom } = this;
+      const value = [
+        'name',
+        'run-at',
+        'homepageURL',
+        'updateURL',
+        'downloadURL',
+      ].reduce((val, key) => {
+        val[key] = custom[key];
+        return val;
+      }, {
+        _include: custom.keepInclude,
+        _match: custom.keepMatch,
+        _exclude: custom.keepExclude,
+        include: toList(custom.include),
+        match: toList(custom.match),
+        exclude: toList(custom.exclude),
+      });
+      return sendMessage({
+        cmd: 'ParseScript',
+        data: {
+          id: this.script.id,
+          code: this.code,
+          // User created scripts MUST be marked `isNew` so that
+          // the backend is able to check namespace conflicts
+          isNew: !this.script.id,
+          message: '',
+          more: {
+            custom: value,
+            update: this.update,
+          },
+        },
+      })
+      .then((script) => {
+        this.script = script;
+        this.canSave = false;
+      }, (err) => {
+        showMessage({ text: err });
+      });
+    },
+    close() {
+      if (!this.canSave || confirm(i18n('confirmNotSaved'))) {
+        this.$emit('close');
+      }
+    },
+    saveClose() {
+      this.save().then(this.close);
+    },
+    contentChange(code) {
+      this.code = code;
+      this.canSave = true;
+    },
+    initEditor(cm) {
+      this.cm = cm;
+      this.bindKeys();
+    },
+    find() {
+      const { state } = this.search;
+      state.posTo = state.posFrom;
+      this.findNext();
+    },
+    findNext(reversed) {
+      const { state } = this.search;
+      const { cm } = this;
+      if (state.query) {
+        findNext(cm, state, reversed);
+      }
+      this.search.show = true;
+      this.$nextTick(() => {
+        this.$refs.search.focus();
+      });
+    },
+    clearSearch() {
+      const { cm } = this;
+      cm.operation(() => {
+        const { state } = this.search;
+        state.posFrom = null;
+        state.posTo = null;
+        this.search.show = false;
+      });
+      cm.focus();
+    },
+    replace(all) {
+      const { cm } = this;
+      const { state } = this.search;
+      if (!state.query) {
+        this.find();
+        return;
+      }
+      (all ? replaceAll : replaceOne)(cm, state);
+    },
+    onKeyDown(e) {
+      const { cm } = this;
+      const name = CodeMirror.keyName(e);
+      const commands = [
+        'cancel',
+        'find',
+        'findNext',
+        'findPrev',
+        'replace',
+        'replaceAll',
+      ];
+      [
+        cm.options.extraKeys,
+        cm.options.keyMap,
+      ].some((keyMap) => {
+        let stop = false;
+        if (keyMap) {
+          CodeMirror.lookupKey(name, keyMap, (b) => {
+            if (commands.includes(b)) {
+              e.preventDefault();
+              e.stopPropagation();
+              cm.execCommand(b);
+              stop = true;
+            }
+          }, cm);
+        }
+        return stop;
+      });
+    },
+    bindKeys() {
+      window.addEventListener('keydown', this.onKeyDown, false);
+    },
+    unbindKeys() {
+      window.removeEventListener('keydown', this.onKeyDown, false);
+    },
+    goToLine() {
+      const line = this.search.line - 1;
+      const { cm } = this;
+      if (!isNaN(line)) cm.setCursor(line, 0);
+      cm.focus();
+    },
+  },
+};
+</script>

+ 0 - 1
src/options/views/editor.html

@@ -1 +0,0 @@
-<div class="editor-code"></div>

+ 0 - 150
src/options/views/editor.js

@@ -1,150 +0,0 @@
-function addScripts(data) {
-  function add(data) {
-    var s = document.createElement('script');
-    if (data.innerHTML) s.innerHTML = data.innerHTML;
-    else if (data.src) s.src = data.src;
-    return new Promise(function (resolve, reject) {
-      s.onload = function () {
-        resolve();
-      };
-      s.onerror = function () {
-        reject();
-      };
-      document.body.appendChild(s);
-    });
-  }
-  if (!data.map) data = [data];
-  return Promise.all(data.map(add));
-}
-
-function addCSS(data) {
-  function add(data) {
-    var s;
-    if (data.html) {
-      s = document.createElement('style');
-      s.innerHTML = data.html;
-    } else if (data.href) {
-      s = document.createElement('link');
-      s.rel = 'stylesheet';
-      s.href = data.href;
-    }
-    if (s) document.body.appendChild(s);
-  }
-  if (!data.forEach) data = [data];
-  data.forEach(add);
-}
-
-function getHandler(key) {
-  return function (cm) {
-    var commands = cm.state.commands;
-    var handle = commands && commands[key];
-    return handle && handle();
-  };
-}
-
-function initCodeMirror() {
-  addCSS([
-    {href: '/public/lib/CodeMirror/lib/codemirror.css'},
-    {href: '/public/lib/CodeMirror/theme/eclipse.css'},
-    {href: '/public/mylib/CodeMirror/fold.css'},
-  ]);
-  return addScripts(
-    {src: '/public/lib/CodeMirror/lib/codemirror.js'}
-  )
-  .then(function () {
-    return addScripts([
-      {src: '/public/lib/CodeMirror/mode/javascript/javascript.js'},
-      {src: '/public/lib/CodeMirror/addon/comment/continuecomment.js'},
-      {src: '/public/lib/CodeMirror/addon/edit/matchbrackets.js'},
-      {src: '/public/lib/CodeMirror/addon/edit/closebrackets.js'},
-      {src: '/public/lib/CodeMirror/addon/fold/foldcode.js'},
-      {src: '/public/lib/CodeMirror/addon/fold/foldgutter.js'},
-      {src: '/public/lib/CodeMirror/addon/fold/brace-fold.js'},
-      {src: '/public/lib/CodeMirror/addon/fold/comment-fold.js'},
-      {src: '/public/lib/CodeMirror/addon/search/match-highlighter.js'},
-      {src: '/public/lib/CodeMirror/addon/search/searchcursor.js'},
-      {src: '/public/lib/CodeMirror/addon/selection/active-line.js'},
-    ]);
-  })
-  .then(function () {
-    [
-      'save', 'cancel', 'find', 'findNext', 'findPrev', 'replace', 'replaceAll',
-    ].forEach(function (key) {
-      CodeMirror.commands[key] = getHandler(key);
-    });
-  });
-}
-
-function indentWithTab(cm) {
-  if (cm.somethingSelected()) {
-    cm.indentSelection('add');
-  } else {
-    cm.replaceSelection(
-      cm.getOption('indentWithTabs') ? '\t' : ' '.repeat(cm.getOption('indentUnit')),
-      'end', '+input');
-  }
-}
-
-var cache = require('../../cache');
-var readyCodeMirror = initCodeMirror();
-
-module.exports = {
-  props: [
-    'readonly',
-    'content',
-    'commands',
-  ],
-  template: cache.get('./editor.html'),
-  mounted: function () {
-    var _this = this;
-    readyCodeMirror.then(function () {
-      var cm = _this.cm = CodeMirror(_this.$el, {
-        continueComments: true,
-        matchBrackets: true,
-        autoCloseBrackets: true,
-        highlightSelectionMatches: true,
-        lineNumbers: true,
-        mode: 'javascript',
-        lineWrapping: true,
-        styleActiveLine: true,
-        foldGutter: true,
-        gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
-        theme: 'eclipse',
-      });
-      _this.readonly && cm.setOption('readOnly', _this.readonly);
-      cm.on('change', function () {
-        _this.cachedContent = cm.getValue();
-        _this.$emit('change', _this.cachedContent);
-      });
-      cm.state.commands = _this.commands;
-      cm.setOption('extraKeys', {
-        Esc: 'cancel',
-        Tab: indentWithTab,
-      });
-      cm.on('keyHandled', function (_cm, _name, e) {
-        e.stopPropagation();
-      });
-      _this.update();
-      _this.$emit('ready', cm);
-    });
-  },
-  watch: {
-    content: function (content) {
-      var _this = this;
-      if (content !== _this.cachedContent) {
-        _this.cachedContent = content;
-        _this.update();
-      }
-    },
-  },
-  methods: {
-    update: function () {
-      var _this = this;
-      var cm = _this.cm;
-      if (!cm || _this.cachedContent == null) return;
-      cm.setValue(_this.cachedContent);
-      cm.getDoc().clearHistory();
-      cm.focus();
-    },
-  },
-};

+ 0 - 15
src/options/views/main.html

@@ -1,15 +0,0 @@
-<div class="main">
-  <aside>
-    <img src="/public/images/icon128.png">
-    <h1 v-text="i18n('extName')"></h1>
-    <hr>
-    <div class=sidemenu>
-      <a href="#?t=Installed" :class="{active:tab==='Main'}" v-text="i18n('sideMenuInstalled')"></a>
-      <a href="#?t=Settings" :class="{active:tab==='Settings'}" v-feature="'settings'">
-        <span v-text="i18n('sideMenuSettings')" class="feature-text"></span>
-      </a>
-      <a href="#?t=About" :class="{active:tab==='About'}" v-text="i18n('sideMenuAbout')"></a>
-    </div>
-  </aside>
-  <component :is="tab"></component>
-</div>

+ 0 - 23
src/options/views/main.js

@@ -1,23 +0,0 @@
-var cache = require('src/cache');
-var MainTab = require('./tab-installed');
-var SettingsTab = require('./tab-settings');
-var AboutTab = require('./tab-about');
-
-var components = {
-  Main: MainTab,
-  Settings: SettingsTab,
-  About: AboutTab,
-};
-
-module.exports = {
-  props: ['params'],
-  template: cache.get('./main.html'),
-  components: components,
-  computed: {
-    tab: function () {
-      var tab = this.params.t;
-      if (!components[tab]) tab = 'Main';
-      return tab;
-    },
-  },
-};

+ 44 - 0
src/options/views/main.vue

@@ -0,0 +1,44 @@
+<template>
+  <div class="main">
+    <aside>
+      <img src="/public/images/icon128.png">
+      <h1 v-text="i18n('extName')"></h1>
+      <hr>
+      <div class=sidemenu>
+        <a href="#?t=Installed" :class="{active: tab === 'Installed'}" v-text="i18n('sideMenuInstalled')"></a>
+        <a href="#?t=Settings" :class="{active: tab === 'Settings'}" v-feature="'settings'">
+          <span v-text="i18n('sideMenuSettings')" class="feature-text"></span>
+        </a>
+        <a href="#?t=About" :class="{active: tab === 'About'}" v-text="i18n('sideMenuAbout')"></a>
+      </div>
+    </aside>
+    <component :is="tab"></component>
+  </div>
+</template>
+
+<script>
+import { store } from '../utils';
+import Installed from './tab-installed';
+import Settings from './tab-settings';
+import About from './tab-about';
+
+const components = {
+  Installed,
+  Settings,
+  About,
+};
+
+export default {
+  components,
+  data() {
+    return store;
+  },
+  computed: {
+    tab() {
+      let tab = this.route.query.t;
+      if (!components[tab]) tab = 'Installed';
+      return tab;
+    },
+  },
+};
+</script>

+ 0 - 4
src/options/views/message.html

@@ -1,4 +0,0 @@
-<transition-group tag="div" name="message">
-  <div v-for="message in messages" class="message"
-  :key="message" v-text="message.text"></div>
-</transition>

+ 0 - 26
src/options/views/message.js

@@ -1,26 +0,0 @@
-var cache = require('../../cache');
-
-function init() {
-  if (div) return;
-  div = document.createElement('div');
-  document.body.appendChild(div);
-  new Vue({
-    el: div,
-    template: cache.get('./message.html'),
-    data: {
-      messages: messages,
-    },
-  });
-}
-
-var div;
-var messages = [];
-
-exports.open = function (options) {
-  init();
-  messages.push(options);
-  setTimeout(function () {
-    var i = messages.indexOf(options);
-    ~i && messages.splice(i, 1);
-  }, 2000);
-};

+ 18 - 0
src/options/views/message.vue

@@ -0,0 +1,18 @@
+<template>
+  <transition-group tag="div" name="message">
+    <div v-for="message in store.messages" class="message"
+    :key="message" v-text="message.text"></div>
+  </transition>
+</template>
+
+<script>
+import { store } from '../utils';
+
+export default {
+  data() {
+    return {
+      store,
+    };
+  },
+};
+</script>

+ 255 - 0
src/options/views/script-item.vue

@@ -0,0 +1,255 @@
+<template>
+  <div class="script" :class="{disabled:!script.enabled}" draggable="true" @dragstart.prevent="onDragStart">
+    <img class="script-icon" :src="safeIcon">
+    <div class="script-info flex">
+      <a class="script-name ellipsis" target=_blank :href="homepageURL"
+      v-text="script.custom.name||getLocaleString('name')"></a>
+      <a class="script-support" v-if="script.meta.supportURL" target=_blank :href="script.meta.supportURL">
+        <svg class="icon"><use xlink:href="#question" /></svg>
+      </a>
+      <div class="flex-auto"></div>
+      <div class="script-author ellipsis" :title="script.meta.author" v-if="author">
+        <span v-text="i18n('labelAuthor')"></span>
+        <a :href="'mailto:'+author.email" v-if="author.email" v-text="author.name"></a>
+        <span v-if="!author.email" v-text="author.name"></span>
+      </div>
+      <div class="script-version" v-text="script.meta.version?'v'+script.meta.version:''"></div>
+    </div>
+    <p class="script-desc ellipsis" v-text="script.custom.description||getLocaleString('description')"></p>
+    <div class=buttons>
+      <button v-text="i18n('buttonEdit')" @click="onEdit"></button>
+      <button @click="onEnable" v-text="labelEnable"></button>
+      <button v-text="i18n('buttonRemove')" @click="onRemove"></button>
+      <button v-if="canUpdate" :disabled="script.checking"
+      v-text="i18n('buttonUpdate')" @click="onUpdate"></button>
+      <span v-text="script.message"></span>
+    </div>
+  </div>
+</template>
+
+<script>
+import { sendMessage, getLocaleString } from 'src/common';
+import { store } from '../utils';
+
+const DEFAULT_ICON = '/public/images/icon48.png';
+const PADDING = 10;
+const SCROLL_GAP = 10;
+
+const images = {};
+function loadImage(url) {
+  if (!url) return Promise.reject();
+  let promise = images[url];
+  if (!promise) {
+    const cache = store.cache[url];
+    promise = cache
+    ? Promise.resolve(cache)
+    : new Promise((resolve, reject) => {
+      const img = new Image();
+      img.onload = () => resolve(url);
+      img.onerror = () => reject(url);
+      img.src = url;
+    });
+    images[url] = promise;
+  }
+  return promise;
+}
+
+export default {
+  props: ['script'],
+  data() {
+    return {
+      safeIcon: DEFAULT_ICON,
+    };
+  },
+  computed: {
+    canUpdate() {
+      const { script } = this;
+      return script.update && (
+        script.custom.updateURL ||
+        script.meta.updateURL ||
+        script.custom.downloadURL ||
+        script.meta.downloadURL ||
+        script.custom.lastInstallURL
+      );
+    },
+    homepageURL() {
+      const { script } = this;
+      return script.custom.homepageURL || script.meta.homepageURL || script.meta.homepage;
+    },
+    author() {
+      const text = this.script.meta.author;
+      if (!text) return;
+      const matches = text.match(/^(.*?)\s<(\S*?@\S*?)>$/);
+      return {
+        email: matches && matches[2],
+        name: matches ? matches[1] : text,
+      };
+    },
+    labelEnable() {
+      return this.script.enabled ? this.i18n('buttonDisable') : this.i18n('buttonEnable');
+    },
+  },
+  mounted() {
+    const { icon } = this.script.meta;
+    if (icon && icon !== this.safeIcon) {
+      loadImage(icon)
+      .then((url) => {
+        this.safeIcon = url;
+      }, () => {
+        this.safeIcon = DEFAULT_ICON;
+      });
+    }
+  },
+  methods: {
+    getLocaleString(key) {
+      return getLocaleString(this.script.meta, key);
+    },
+    onEdit() {
+      this.$emit('edit', this.script.id);
+    },
+    onRemove() {
+      sendMessage({
+        cmd: 'RemoveScript',
+        data: this.script.id,
+      });
+    },
+    onEnable() {
+      sendMessage({
+        cmd: 'UpdateScriptInfo',
+        data: {
+          id: this.script.id,
+          enabled: this.script.enabled ? 0 : 1,
+        },
+      });
+    },
+    onUpdate() {
+      sendMessage({
+        cmd: 'CheckUpdate',
+        data: this.script.id,
+      });
+    },
+    onDragStart(e) {
+      const el = e.currentTarget;
+      const parent = el.parentNode;
+      const rect = el.getBoundingClientRect();
+      const next = el.nextElementSibling;
+      const dragging = {
+        el,
+        offset: {
+          x: e.clientX - rect.left,
+          y: e.clientY - rect.top,
+        },
+        delta: (next ? next.getBoundingClientRect().top : parent.offsetHeight) - rect.top,
+        index: [].indexOf.call(parent.children, el),
+        elements: [].filter.call(parent.children, child => child !== el),
+        dragged: el.cloneNode(true),
+      };
+      this.dragging = dragging;
+      dragging.lastIndex = dragging.index;
+      const { dragged } = dragging;
+      dragged.classList.add('dragging');
+      dragged.style.left = `${rect.left}px`;
+      dragged.style.top = `${rect.top}px`;
+      dragged.style.width = `${rect.width}px`;
+      parent.appendChild(dragged);
+      el.classList.add('dragging-placeholder');
+      document.addEventListener('mousemove', this.onDragMouseMove, false);
+      document.addEventListener('mouseup', this.onDragMouseUp, false);
+    },
+    onDragMouseMove(e) {
+      const { dragging } = this;
+      const { el, dragged, offset, elements, lastIndex } = dragging;
+      dragged.style.left = `${e.clientX - offset.x}px`;
+      dragged.style.top = `${e.clientY - offset.y}px`;
+      let hoveredIndex = elements.findIndex((item) => {
+        if (!item) return;
+        if (item.classList.contains('dragging-moving')) return;
+        const rect = item.getBoundingClientRect();
+        return (
+          e.clientX >= rect.left + PADDING
+          && e.clientX <= rect.left + rect.width - PADDING
+          && e.clientY >= rect.top + PADDING
+          && e.clientY <= rect.top + rect.height - PADDING
+        );
+      });
+      if (hoveredIndex >= 0) {
+        const hoveredEl = elements[hoveredIndex];
+        const isDown = hoveredIndex >= lastIndex;
+        let { delta } = dragging;
+        if (isDown) {
+          hoveredIndex += 1;
+          hoveredEl.parentNode.insertBefore(el, hoveredEl.nextElementSibling);
+        } else {
+          delta = -delta;
+          hoveredEl.parentNode.insertBefore(el, hoveredEl);
+        }
+        dragging.lastIndex = hoveredIndex;
+        this.onDragAnimate(dragging.elements.slice(
+          isDown ? lastIndex : hoveredIndex,
+          isDown ? hoveredIndex : lastIndex,
+        ), delta);
+      }
+      this.onDragScrollCheck(e.clientY);
+    },
+    onDragMouseUp() {
+      document.removeEventListener('mousemove', this.onDragMouseMove, false);
+      document.removeEventListener('mouseup', this.onDragMouseUp, false);
+      const { dragging } = this;
+      this.dragging = null;
+      dragging.dragged.remove();
+      dragging.el.classList.remove('dragging-placeholder');
+      this.$emit('move', {
+        from: dragging.index,
+        to: dragging.lastIndex,
+      });
+    },
+    onDragAnimate(elements, delta) {
+      elements.forEach((el) => {
+        if (!el) return;
+        el.classList.add('dragging-moving');
+        el.style.transition = 'none';
+        el.style.transform = `translateY(${delta}px)`;
+        el.addEventListener('transitionend', endAnimation, false);
+        setTimeout(() => {
+          el.style.transition = '';
+          el.style.transform = '';
+        });
+      });
+      function endAnimation(e) {
+        e.target.classList.remove('dragging-moving');
+        e.target.removeEventListener('transitionend', endAnimation, false);
+      }
+    },
+    onDragScrollCheck(y) {
+      const { dragging } = this;
+      let scrollSpeed = 0;
+      const offset = dragging.el.parentNode.getBoundingClientRect();
+      let delta = (y - (offset.bottom - SCROLL_GAP)) / SCROLL_GAP;
+      if (delta > 0) {
+        // scroll down
+        scrollSpeed = 1 + Math.min((delta * 5) | 0, 10);
+      } else {
+        // scroll up
+        delta = (offset.top + SCROLL_GAP - y) / SCROLL_GAP;
+        if (delta > 0) scrollSpeed = -1 - Math.min((delta * 5) | 0, 10);
+      }
+      dragging.scrollSpeed = scrollSpeed;
+      if (scrollSpeed) this.onDragScroll();
+    },
+    onDragScroll() {
+      const scroll = () => {
+        const { dragging } = this;
+        if (!dragging) return;
+        if (dragging.scrollSpeed) {
+          dragging.el.parentNode.scrollTop += dragging.scrollSpeed;
+          setTimeout(scroll, 32);
+        } else dragging.scrolling = false;
+      };
+      if (this.dragging && !this.dragging.scrolling) {
+        this.dragging.scrolling = true;
+        scroll();
+      }
+    },
+  },
+};
+</script>

+ 0 - 27
src/options/views/script.html

@@ -1,27 +0,0 @@
-<div class="script" :class="{disabled:!script.enabled}" draggable="true">
-  <img class="script-icon" :src="safeIcon">
-  <div class="script-info flex">
-    <a class="script-name ellipsis" target=_blank :href="homepageURL"
-      v-text="script.custom.name||getLocaleString('name')"></a>
-    <a class="script-support" v-if="script.meta.supportURL"
-      target=_blank :href="script.meta.supportURL">
-      <svg class="icon"><use xlink:href="#question"/></svg>
-    </a>
-    <div class="flex-auto"></div>
-    <div class="script-author ellipsis" :title="script.meta.author" v-if="author">
-      <span v-text="i18n('labelAuthor')"></span>
-      <a :href="'mailto:'+author.email" v-if="author.email" v-text="author.name"></a>
-      <span v-if="!author.email" v-text="author.name"></span>
-    </div>
-    <div class="script-version" v-text="script.meta.version?'v'+script.meta.version:''"></div>
-  </div>
-  <p class="script-desc ellipsis" v-text="script.custom.description||getLocaleString('description')"></p>
-  <div class=buttons>
-    <button v-text="i18n('buttonEdit')" @click="onEdit"></button>
-    <button @click="onEnable" v-text="labelEnable"></button>
-    <button v-text="i18n('buttonRemove')" @click="onRemove"></button>
-    <button v-if="canUpdate" :disabled="script.checking"
-      v-text="i18n('buttonUpdate')" @click="onUpdate"></button>
-    <span v-text="script.message"></span>
-  </div>
-</div>

+ 0 - 254
src/options/views/script.js

@@ -1,254 +0,0 @@
-var utils = require('../utils');
-var cache = require('../../cache');
-var _ = require('../../common');
-var store = utils.store;
-
-var DEFAULT_ICON = '/public/images/icon48.png';
-
-module.exports = {
-  props: ['script'],
-  template: cache.get('./script.html'),
-  data: function () {
-    return {
-      safeIcon: DEFAULT_ICON,
-    };
-  },
-  computed: {
-    canUpdate: function () {
-      var script = this.script;
-      return script.update && (
-        script.custom.updateURL ||
-        script.meta.updateURL ||
-        script.custom.downloadURL ||
-        script.meta.downloadURL ||
-        script.custom.lastInstallURL
-      );
-    },
-    homepageURL: function () {
-      var script = this.script;
-      return script.custom.homepageURL || script.meta.homepageURL || script.meta.homepage;
-    },
-    author: function () {
-      var text = this.script.meta.author;
-      if (!text) return;
-      var matches = text.match(/^(.*?)\s<(\S*?@\S*?)>$/);
-      return {
-        email: matches && matches[2],
-        name: matches ? matches[1] : text,
-      };
-    },
-    labelEnable: function () {
-      return this.script.enabled ? _.i18n('buttonDisable') : _.i18n('buttonEnable');
-    },
-  },
-  mounted: function () {
-    var _this = this;
-    _this.$el.addEventListener('dragstart', _this.onDragStart.bind(_this), false);
-    var icon = _this.script.meta.icon;
-    if (icon && icon !== _this.safeIcon) {
-      _this.loadImage(icon)
-      .then(function (url) {
-        _this.safeIcon = url;
-      }, function () {
-        _this.safeIcon = DEFAULT_ICON;
-      });
-    }
-  },
-  methods: {
-    getLocaleString: function (key) {
-      return _.getLocaleString(this.script.meta, key);
-    },
-    loadImage: function () {
-      var images = {};
-      return function (url) {
-        if (!url) return Promise.reject();
-        var promise = images[url];
-        if (!promise) {
-          var cache = store.cache[url];
-          promise = cache ? Promise.resolve(cache)
-            : new Promise(function (resolve, reject) {
-              var img = new Image;
-              img.onload = function () {
-                resolve(url);
-              };
-              img.onerror = function () {
-                reject(url);
-              };
-              img.src = url;
-            });
-          images[url] = promise;
-        }
-        return promise;
-      };
-    }(),
-    onEdit: function () {
-      var _this = this;
-      _this.$emit('edit', _this.script.id);
-    },
-    onRemove: function () {
-      var _this = this;
-      _.sendMessage({
-        cmd: 'RemoveScript',
-        data: _this.script.id,
-      });
-    },
-    onEnable: function () {
-      var _this = this;
-      _.sendMessage({
-        cmd: 'UpdateScriptInfo',
-        data: {
-          id: _this.script.id,
-          enabled: _this.script.enabled ? 0 : 1,
-        },
-      });
-    },
-    onUpdate: function () {
-      _.sendMessage({
-        cmd: 'CheckUpdate',
-        data: this.script.id,
-      });
-    },
-    onDragStart: function (e) {
-      var _this = this;
-      new DND(e, function (data) {
-        _this.$emit('move', data);
-      });
-    },
-  },
-};
-
-function DND(e, cb) {
-  var _this = this;
-  _this.mousemove = _this.mousemove.bind(_this);
-  _this.mouseup = _this.mouseup.bind(_this);
-  if (e) {
-    e.preventDefault();
-    _this.start(e);
-  }
-  _this.onDrop = cb;
-}
-DND.prototype.start = function (e) {
-  var _this = this;
-  var dragging = _this.dragging = {};
-  var el = dragging.el = e.currentTarget;
-  var parent = el.parentNode;
-  var rect = el.getBoundingClientRect();
-  dragging.offset = {
-    x: e.clientX - rect.left,
-    y: e.clientY - rect.top,
-  };
-  var next = el.nextElementSibling;
-  dragging.delta = (next ? next.getBoundingClientRect().top : parent.offsetHeight) - rect.top;
-  dragging.lastIndex = dragging.index = [].indexOf.call(parent.children, el);
-  dragging.elements = [].filter.call(parent.children, function (el) {
-    return el !== dragging.el;
-  });
-  var dragged = dragging.dragged = el.cloneNode(true);
-  dragged.classList.add('dragging');
-  dragged.style.left = rect.left + 'px';
-  dragged.style.top = rect.top + 'px';
-  dragged.style.width = rect.width + 'px';
-  parent.appendChild(dragged);
-  el.classList.add('dragging-placeholder');
-  document.addEventListener('mousemove', _this.mousemove, false);
-  document.addEventListener('mouseup', _this.mouseup, false);
-};
-DND.prototype.mousemove = function (e) {
-  var _this = this;
-  var dragging = _this.dragging;
-  var dragged = dragging.dragged;
-  dragged.style.left = e.clientX - dragging.offset.x + 'px';
-  dragged.style.top = e.clientY - dragging.offset.y + 'px';
-  var hoveredIndex = dragging.elements.findIndex(function (el) {
-    if (!el) return;
-    if (el.classList.contains('dragging-moving')) return;
-    var rect = el.getBoundingClientRect();
-    var pad = 10;
-    return (
-      e.clientX >= rect.left + pad
-      && e.clientX <= rect.left + rect.width - pad
-      && e.clientY >= rect.top + pad
-      && e.clientY <= rect.top + rect.height - pad
-    );
-  });
-  if (~hoveredIndex) {
-    var hoveredEl = dragging.elements[hoveredIndex];
-    var lastIndex = dragging.lastIndex;
-    var isDown = hoveredIndex >= lastIndex;
-    var el = dragging.el;
-    var delta = dragging.delta;
-    if (isDown) {
-      hoveredIndex ++;
-      hoveredEl.parentNode.insertBefore(el, hoveredEl.nextElementSibling);
-    } else {
-      delta = -delta;
-      hoveredEl.parentNode.insertBefore(el, hoveredEl);
-    }
-    dragging.lastIndex = hoveredIndex;
-    _this.animate(dragging.elements.slice(
-      isDown ? lastIndex : hoveredIndex,
-      isDown ? hoveredIndex : lastIndex
-    ), delta);
-  }
-  _this.checkScroll(e.clientY);
-};
-DND.prototype.animate = function (elements, delta) {
-  function endAnimation(e) {
-    e.target.classList.remove('dragging-moving');
-    e.target.removeEventListener('transitionend', endAnimation, false);
-  }
-  elements.forEach(function (el) {
-    if (!el) return;
-    el.classList.add('dragging-moving');
-    el.style.transition = 'none';
-    el.style.transform = 'translateY(' + delta + 'px)';
-    el.addEventListener('transitionend', endAnimation, false);
-    setTimeout(function () {
-      el.style.transition = '';
-      el.style.transform = '';
-    });
-  });
-};
-DND.prototype.mouseup = function () {
-  var _this = this;
-  document.removeEventListener('mousemove', _this.mousemove, false);
-  document.removeEventListener('mouseup', _this.mouseup, false);
-  var dragging = _this.dragging;
-  dragging.dragged.remove();
-  dragging.el.classList.remove('dragging-placeholder');
-  _this.dragging = null;
-  _this.onDrop && _this.onDrop({
-    from: dragging.index,
-    to: dragging.lastIndex,
-  });
-};
-DND.prototype.checkScroll = function (y) {
-  var dragging = this.dragging;
-  var scrollThreshold = 10;
-  dragging.scroll = 0;
-  var offset = dragging.el.parentNode.getBoundingClientRect();
-  var delta = (y - (offset.bottom - scrollThreshold)) / scrollThreshold;
-  if (delta > 0) {
-    dragging.scroll = 1 + Math.min(~~ (delta * 5), 10);
-  } else {
-    delta = (offset.top + scrollThreshold - y) / scrollThreshold;
-    if (delta > 0) dragging.scroll = -1 - Math.min(~~ (delta * 5), 10);
-  }
-  if (dragging.scroll) this.scrollParent();
-};
-DND.prototype.scrollParent = function () {
-  function scroll() {
-    var dragging = _this.dragging;
-    if (dragging) {
-      if (dragging.scroll) {
-        dragging.el.parentNode.scrollTop += dragging.scroll;
-        setTimeout(scroll, 32);
-      } else dragging.scrolling = false;
-    }
-  }
-  var _this = this;
-  if (!_this.dragging.scrolling) {
-    _this.dragging.scrolling = true;
-    scroll();
-  }
-};

+ 0 - 28
src/options/views/tab-about.html

@@ -1,28 +0,0 @@
-<div class="content">
-  <h1>
-    <span v-text="i18n('labelAbout')"></span>
-    <small v-text="'v'+version"></small>
-  </h1>
-  <p class="mb-2" v-text="i18n('extDescription')"></p>
-  <div class="mb-2">
-    <label v-text="i18n('labelRelated')"></label>
-    <a href="https://violentmonkey.github.io" target="_blank" v-text="i18n('extName')"></a> |
-    <a href="https://violentmonkey.github.io/donate/" target="_blank" v-text="i18n('labelDonate')"></a> |
-    <a href="https://github.com/violentmonkey/violentmonkey/issues" target="_blank" v-text="i18n('labelFeedback')"></a>
-  </div>
-  <div class="mb-2">
-    <label v-text="i18n('labelAuthor')"></label>
-    <span v-html="i18n('anchorAuthor')"></span>
-  </div>
-  <div class="mb-2">
-    <label v-text="i18n('labelTranslator')"></label>
-    <span v-html="i18n('anchorTranslator')"></span>
-  </div>
-  <div class="mb-2">
-    <label v-text="i18n('labelCurrentLang')"></label>
-    <span id="currentLang" v-text="language"></span> |
-    <a href="https://violentmonkey.github.io/localization/" target="_blank">
-      Help with translation
-    </a>
-  </div>
-</div>

+ 0 - 12
src/options/views/tab-about.js

@@ -1,12 +0,0 @@
-var cache = require('src/cache');
-var data = {
-  version: browser.runtime.getManifest().version,
-  language: navigator.language,
-};
-
-module.exports = {
-  template: cache.get('./tab-about.html'),
-  data: function () {
-    return data;
-  },
-};

+ 43 - 0
src/options/views/tab-about.vue

@@ -0,0 +1,43 @@
+<template>
+  <div class="content">
+    <h1>
+      <span v-text="i18n('labelAbout')"></span>
+      <small v-text="`v${version}`"></small>
+    </h1>
+    <p class="mb-2" v-text="i18n('extDescription')"></p>
+    <div class="mb-2">
+      <label v-text="i18n('labelRelated')"></label>
+      <a href="https://violentmonkey.github.io" target="_blank" v-text="i18n('extName')"></a> |
+      <a href="https://violentmonkey.github.io/donate/" target="_blank" v-text="i18n('labelDonate')"></a> |
+      <a href="https://github.com/violentmonkey/violentmonkey/issues" target="_blank" v-text="i18n('labelFeedback')"></a>
+    </div>
+    <div class="mb-2">
+      <label v-text="i18n('labelAuthor')"></label>
+      <span v-html="i18n('anchorAuthor')"></span>
+    </div>
+    <div class="mb-2">
+      <label v-text="i18n('labelTranslator')"></label>
+      <span v-html="i18n('anchorTranslator')"></span>
+    </div>
+    <div class="mb-2">
+      <label v-text="i18n('labelCurrentLang')"></label>
+      <span id="currentLang" v-text="language"></span> |
+      <a href="https://violentmonkey.github.io/localization/" target="_blank">
+        Help with translation
+      </a>
+    </div>
+  </div>
+</template>
+
+<script>
+const data = {
+  version: browser.runtime.getManifest().version,
+  language: navigator.language,
+};
+
+export default {
+  data() {
+    return data;
+  },
+};
+</script>

+ 0 - 17
src/options/views/tab-installed.html

@@ -1,17 +0,0 @@
-<div class="content no-pad">
-  <header>
-    <button v-text="i18n('buttonNew')" @click="newScript"></button>
-    <button v-text="i18n('buttonUpdateAll')" @click="updateAll"></button>
-    <button v-text="i18n('buttonInstallFromURL')" @click="installFromURL"></button>
-    <div class="pull-right">
-      <a href=https://greasyfork.org/scripts target=_blank v-text="i18n('anchorGetMoreScripts')"></a>
-    </div>
-  </header>
-  <div class="backdrop" :class="{mask:store.loading}" v-show="message">
-    <div v-html="message"></div>
-  </div>
-  <div class="scripts">
-    <script-item v-for="script in store.scripts" :key="script" :script="script" @edit="editScript" @move="moveScript"></script-item>
-  </div>
-  <edit v-if="script" :script="script" @close="endEditScript"></edit>
-</div>

+ 0 - 85
src/options/views/tab-installed.js

@@ -1,85 +0,0 @@
-var ScriptItem = require('./script');
-var Edit = require('./edit');
-var cache = require('../../cache');
-var _ = require('../../common');
-var utils = require('../utils');
-var store = utils.store;
-
-module.exports = {
-  template: cache.get('./tab-installed.html'),
-  components: {
-    ScriptItem: ScriptItem,
-    Edit: Edit,
-  },
-  data: function () {
-    return {
-      script: null,
-      store: store,
-    };
-  },
-  computed: {
-    message: function () {
-      var _this = this;
-      if (_this.store.loading) {
-        return _.i18n('msgLoading');
-      }
-      if (!_this.store.scripts.length) {
-        return _.i18n('labelNoScripts');
-      }
-    },
-  },
-  methods: {
-    newScript: function () {
-      var _this = this;
-      _.sendMessage({cmd: 'NewScript'})
-      .then(function (script) {
-        _this.script = script;
-      });
-    },
-    updateAll: function () {
-      _.sendMessage({cmd: 'CheckUpdateAll'});
-    },
-    installFromURL: function () {
-      var url = prompt(_.i18n('hintInputURL'));
-      if (url && ~url.indexOf('://')) {
-        browser.tabs.create({
-          url: browser.runtime.getURL(browser.runtime.getManifest().options_page) + '#confirm?u=' + encodeURIComponent(url),
-        });
-      }
-    },
-    editScript: function (id) {
-      var _this = this;
-      _this.script = _this.store.scripts.find(function (script) {
-        return script.id === id;
-      });
-    },
-    endEditScript: function () {
-      this.script = null;
-    },
-    moveScript: function (data) {
-      var _this = this;
-      if (data.from === data.to) return;
-      _.sendMessage({
-        cmd: 'Move',
-        data: {
-          id: _this.store.scripts[data.from].id,
-          offset: data.to - data.from,
-        },
-      })
-      .then(function () {
-        var scripts = _this.store.scripts;
-        var i = Math.min(data.from, data.to);
-        var j = Math.max(data.from, data.to);
-        var seq = [
-          scripts.slice(0, i),
-          scripts.slice(i, j + 1),
-          scripts.slice(j + 1),
-        ];
-        i === data.to
-          ? seq[1].unshift(seq[1].pop())
-          : seq[1].push(seq[1].shift());
-        _this.store.scripts = seq.concat.apply([], seq);
-      });
-    },
-  },
-};

+ 101 - 0
src/options/views/tab-installed.vue

@@ -0,0 +1,101 @@
+<template>
+  <div class="content no-pad">
+    <header class="flex">
+      <button v-text="i18n('buttonNew')" @click="newScript"></button>
+      <button v-text="i18n('buttonUpdateAll')" @click="updateAll"></button>
+      <button v-text="i18n('buttonInstallFromURL')" @click="installFromURL"></button>
+      <div class="flex-auto"></div>
+      <a href="https://greasyfork.org/scripts" target="_blank" v-text="i18n('anchorGetMoreScripts')"></a>
+    </header>
+    <div class="backdrop" :class="{mask: store.loading}" v-show="message">
+      <div v-html="message"></div>
+    </div>
+    <div class="scripts">
+      <item v-for="script in store.scripts" :key="script"
+      :script="script" @edit="editScript" @move="moveScript"></item>
+    </div>
+    <edit v-if="script" :script="script" @close="endEditScript"></edit>
+  </div>
+</template>
+
+<script>
+import { i18n, sendMessage } from 'src/common';
+import Item from './script';
+import Edit from './edit';
+import { store } from '../utils';
+
+export default {
+  components: {
+    Item,
+    Edit,
+  },
+  data() {
+    return {
+      store,
+      script: null,
+    };
+  },
+  computed: {
+    message() {
+      if (this.store.loading) {
+        return i18n('msgLoading');
+      }
+      if (!this.store.scripts.length) {
+        return i18n('labelNoScripts');
+      }
+    },
+  },
+  methods: {
+    newScript() {
+      sendMessage({ cmd: 'NewScript' })
+      .then((script) => {
+        this.script = script;
+      });
+    },
+    updateAll() {
+      sendMessage({ cmd: 'CheckUpdateAll' });
+    },
+    installFromURL() {
+      const url = prompt(i18n('hintInputURL'));
+      if (url && url.includes('://')) {
+        const urlOptions = browser.runtime.getURL(browser.runtime.getManifest().options_page);
+        browser.tabs.create({
+          url: `${urlOptions}#confirm?u=${encodeURIComponent(url)}`,
+        });
+      }
+    },
+    editScript(id) {
+      this.script = this.store.scripts.find(script => script.id === id);
+    },
+    endEditScript() {
+      this.script = null;
+    },
+    moveScript(data) {
+      if (data.from === data.to) return;
+      sendMessage({
+        cmd: 'Move',
+        data: {
+          id: this.store.scripts[data.from].id,
+          offset: data.to - data.from,
+        },
+      })
+      .then(() => {
+        const { scripts } = this.store;
+        const i = Math.min(data.from, data.to);
+        const j = Math.max(data.from, data.to);
+        const seq = [
+          scripts.slice(0, i),
+          scripts.slice(i, j + 1),
+          scripts.slice(j + 1),
+        ];
+        if (i === data.to) {
+          seq[1].unshift(seq[1].pop());
+        } else {
+          seq[1].push(seq[1].shift());
+        }
+        this.store.scripts = seq.concat.apply([], seq);
+      });
+    },
+  },
+};
+</script>

+ 0 - 35
src/options/views/tab-settings/index.html

@@ -1,35 +0,0 @@
-<div class="content tab-settings">
-  <h1 v-text="i18n('labelSettings')"></h1>
-  <section>
-    <h3 v-text="i18n('labelGeneral')"></h3>
-    <div class="mb-1">
-      <label>
-        <input type=checkbox v-setting="'autoUpdate'" @change="updateAutoUpdate">
-        <span v-text="i18n('labelAutoUpdate')"></span>
-      </label>
-    </div>
-    <div class="mb-1">
-      <label>
-        <input type=checkbox v-setting="'showBadge'">
-        <span v-text="i18n('labelShowBadge')"></span>
-      </label>
-    </div>
-    <div class="mb-1">
-      <label>
-        <input type=checkbox v-setting="'ignoreGrant'">
-        <span v-text="i18n('labelIgnoreGrant')"></span>
-      </label>
-    </div>
-    <div class="mb-1">
-      <label>
-        <input type=checkbox v-setting="'autoReload'">
-        <span v-text="i18n('labelAutoReloadCurrentTab')"></span>
-      </label>
-    </div>
-  </section>
-  <vm-import></vm-import>
-  <vm-export></vm-export>
-  <vm-sync></vm-sync>
-  <vm-blacklist></vm-blacklist>
-  <vm-css></vm-css>
-</div>

+ 0 - 23
src/options/views/tab-settings/index.js

@@ -1,23 +0,0 @@
-var _ = require('src/common');
-var cache = require('src/cache');
-var VmImport = require('./vm-import');
-var VmExport = require('./vm-export');
-var VmSync = require('./vm-sync');
-var VmBlacklist = require('./vm-blacklist');
-var VmCss = require('./vm-css');
-
-module.exports = {
-  template: cache.get('./index.html'),
-  components: {
-    VmImport: VmImport,
-    VmExport: VmExport,
-    VmSync: VmSync,
-    VmBlacklist: VmBlacklist,
-    VmCss: VmCss,
-  },
-  methods: {
-    updateAutoUpdate: function () {
-      _.sendMessage({cmd: 'AutoUpdate'});
-    },
-  },
-};

+ 61 - 0
src/options/views/tab-settings/index.vue

@@ -0,0 +1,61 @@
+<template>
+  <div class="content tab-settings">
+    <h1 v-text="i18n('labelSettings')"></h1>
+    <section>
+      <h3 v-text="i18n('labelGeneral')"></h3>
+      <div class="mb-1">
+        <label>
+          <input type=checkbox v-setting="'autoUpdate'" @change="updateAutoUpdate">
+          <span v-text="i18n('labelAutoUpdate')"></span>
+        </label>
+      </div>
+      <div class="mb-1">
+        <label>
+          <input type=checkbox v-setting="'showBadge'">
+          <span v-text="i18n('labelShowBadge')"></span>
+        </label>
+      </div>
+      <div class="mb-1">
+        <label>
+          <input type=checkbox v-setting="'ignoreGrant'">
+          <span v-text="i18n('labelIgnoreGrant')"></span>
+        </label>
+      </div>
+      <div class="mb-1">
+        <label>
+          <input type=checkbox v-setting="'autoReload'">
+          <span v-text="i18n('labelAutoReloadCurrentTab')"></span>
+        </label>
+      </div>
+    </section>
+    <vm-import></vm-import>
+    <vm-export></vm-export>
+    <vm-sync></vm-sync>
+    <vm-blacklist></vm-blacklist>
+    <vm-css></vm-css>
+  </div>
+</template>
+
+<script>
+import { sendMessage } from 'src/common';
+import VmImport from './vm-import';
+import VmExport from './vm-export';
+import VmSync from './vm-sync';
+import VmBlacklist from './vm-blacklist';
+import VmCss from './vm-css';
+
+export default {
+  components: {
+    VmImport,
+    VmExport,
+    VmSync,
+    VmBlacklist,
+    VmCss,
+  },
+  methods: {
+    updateAutoUpdate() {
+      sendMessage({ cmd: 'AutoUpdate' });
+    },
+  },
+};
+</script>

+ 37 - 0
src/options/views/tab-settings/vm-blacklist.vue

@@ -0,0 +1,37 @@
+<template>
+  <section v-feature="'blacklist'">
+    <h3>
+      <span class="feature-text" v-text="i18n('labelBlacklist')"></span>
+    </h3>
+    <p v-html="i18n('descBlacklist')"></p>
+    <textarea v-model="rules"></textarea>
+    <button v-text="i18n('buttonSaveBlacklist')" @click="onSave"></button>
+  </section>
+</template>
+
+<script>
+import { i18n, sendMessage } from 'src/common';
+import options from 'src/common/options';
+import { showMessage } from '../../utils';
+
+export default {
+  data() {
+    const rules = options.get('blacklist') || [];
+    return {
+      rules: rules.join('\n'),
+    };
+  },
+  methods: {
+    onSave() {
+      const rules = this.rules.split('\n')
+      .map(item => item.trim())
+      .filter(Boolean);
+      options.set('blacklist', rules);
+      showMessage({
+        text: i18n('msgSavedBlacklist'),
+      });
+      sendMessage({ cmd: 'BlacklistReset' });
+    },
+  },
+};
+</script>

+ 0 - 8
src/options/views/tab-settings/vm-blacklist/index.html

@@ -1,8 +0,0 @@
-<section v-feature="'blacklist'">
-  <h3>
-    <span class="feature-text" v-text="i18n('labelBlacklist')"></span>
-  </h3>
-  <p v-html="i18n('descBlacklist')"></p>
-  <textarea v-model="rules"></textarea>
-  <button v-text="i18n('buttonSaveBlacklist')" @click="onSave"></button>
-</section>

+ 0 - 25
src/options/views/tab-settings/vm-blacklist/index.js

@@ -1,25 +0,0 @@
-var _ = require('src/common');
-var cache = require('src/cache');
-var Message = require('src/options/views/message');
-
-module.exports = {
-  template: cache.get('./index.html'),
-  data: function () {
-    var rules = _.options.get('blacklist') || [];
-    return {
-      rules: rules.join('\n'),
-    };
-  },
-  methods: {
-    onSave: function () {
-      var rules = this.rules.split('\n')
-      .map(function (item) {return item.trim();})
-      .filter(Boolean);
-      _.options.set('blacklist', rules);
-      Message.open({
-        text: _.i18n('msgSavedBlacklist'),
-      });
-      _.sendMessage({cmd: 'BlacklistReset'});
-    },
-  },
-};

+ 32 - 0
src/options/views/tab-settings/vm-css.vue

@@ -0,0 +1,32 @@
+<template>
+  <section v-feature="'css'">
+    <h3>
+      <span class="feature-text" v-text="i18n('labelCustomCSS')"></span>
+    </h3>
+    <p v-html="i18n('descCustomCSS')"></p>
+    <textarea v-model="css"></textarea>
+    <button v-text="i18n('buttonSaveCustomCSS')" @click="onSave"></button>
+  </section>
+</template>
+
+<script>
+import { i18n } from 'src/common';
+import options from 'src/common/options';
+import { showMessage } from '../../utils';
+
+export default {
+  data() {
+    return {
+      css: options.get('customCSS'),
+    };
+  },
+  methods: {
+    onSave() {
+      options.set('customCSS', (this.css || '').trim());
+      showMessage({
+        text: i18n('msgSavedCustomCSS'),
+      });
+    },
+  },
+};
+</script>

+ 0 - 8
src/options/views/tab-settings/vm-css/index.html

@@ -1,8 +0,0 @@
-<section v-feature="'css'">
-  <h3>
-    <span class="feature-text" v-text="i18n('labelCustomCSS')"></span>
-  </h3>
-  <p v-html="i18n('descCustomCSS')"></p>
-  <textarea v-model="css"></textarea>
-  <button v-text="i18n('buttonSaveCustomCSS')" @click="onSave"></button>
-</section>

+ 0 - 20
src/options/views/tab-settings/vm-css/index.js

@@ -1,20 +0,0 @@
-var _ = require('src/common');
-var cache = require('src/cache');
-var Message = require('src/options/views/message');
-
-module.exports = {
-  template: cache.get('./index.html'),
-  data: function () {
-    return {
-      css: _.options.get('customCSS'),
-    };
-  },
-  methods: {
-    onSave: function () {
-      _.options.set('customCSS', (this.css || '').trim());
-      Message.open({
-        text: _.i18n('msgSavedCustomCSS'),
-      });
-    },
-  },
-};

+ 142 - 0
src/options/views/tab-settings/vm-export.vue

@@ -0,0 +1,142 @@
+<template>
+  <section>
+    <h3 v-text="i18n('labelDataExport')"></h3>
+    <select class=export-list multiple v-model="selectedIds">
+      <option class="ellipsis" v-for="script in store.scripts"
+      :value="script.id" v-text="script.custom.name||script.meta.name"></option>
+    </select>
+    <button v-text="i18n('buttonAllNone')" @click="updateSelection()"></button>
+    <button v-text="i18n('buttonExportData')" @click="exportData" :disabled="exporting"></button>
+    <label>
+      <input type=checkbox v-setting="'exportValues'">
+      <span v-text="i18n('labelExportScriptData')"></span>
+    </label>
+  </section>
+</template>
+
+<script>
+import { sendMessage } from 'src/common';
+import options from 'src/common/options';
+import { store } from '../../utils';
+
+export default {
+  data() {
+    return {
+      store,
+      selectedIds: [],
+      exporting: false,
+    };
+  },
+  watch: {
+    'store.scripts'() {
+      this.updateSelection(true);
+    },
+  },
+  created() {
+    this.updateSelection(true);
+  },
+  methods: {
+    updateSelection(selectAll) {
+      if (!store.scripts.length) return;
+      if (selectAll || this.selectedIds.length < store.scripts.length) {
+        this.selectedIds = store.scripts.map(script => script.id);
+      } else {
+        this.selectedIds = [];
+      }
+    },
+    exportData() {
+      this.exporting = true;
+      Promise.resolve(exportData(this.selectedIds))
+      .catch((err) => {
+        console.error(err);
+      })
+      .then(() => {
+        this.exporting = false;
+      });
+    },
+  },
+};
+
+function getWriter() {
+  return new Promise((resolve) => {
+    zip.createWriter(new zip.BlobWriter(), (writer) => {
+      resolve(writer);
+    });
+  });
+}
+
+function addFile(writer, file) {
+  return new Promise((resolve) => {
+    writer.add(file.name, new zip.TextReader(file.content), () => {
+      resolve(writer);
+    });
+  });
+}
+
+function download(writer) {
+  return new Promise((resolve) => {
+    writer.close((blob) => {
+      resolve(blob);
+    });
+  })
+  .then((blob) => {
+    const url = URL.createObjectURL(blob);
+    const a = document.createElement('a');
+    a.href = url;
+    a.download = 'scripts.zip';
+    a.click();
+    setTimeout(() => {
+      URL.revokeObjectURL(url);
+    });
+  });
+}
+
+function exportData(selectedIds) {
+  if (!selectedIds.length) return;
+  const withValues = options.get('exportValues');
+  return sendMessage({
+    cmd: 'ExportZip',
+    data: {
+      values: withValues,
+      ids: selectedIds,
+    },
+  })
+  .then((data) => {
+    const names = {};
+    const vm = {
+      scripts: {},
+      settings: options.get(),
+    };
+    if (withValues) vm.values = {};
+    const files = data.scripts.map((script) => {
+      let name = script.custom.name || script.meta.name || 'Noname';
+      if (names[name]) {
+        names[name] += 1;
+        name = `${name}_${names[name]}`;
+      } else names[name] = 1;
+      vm.scripts[name] = ['id', 'custom', 'enabled', 'update']
+      .reduce((res, key) => {
+        res[key] = script[key];
+        return res;
+      }, {});
+      if (withValues) {
+        const values = data.values[script.uri];
+        if (values) vm.values[script.uri] = values;
+      }
+      return {
+        name: `${name}.user.js`,
+        content: script.code,
+      };
+    });
+    files.push({
+      name: 'ViolentMonkey',
+      content: JSON.stringify(vm),
+    });
+    return files;
+  })
+  .then(files => files.reduce((result, file) => (
+    result.then(writer => addFile(writer, file))
+  ), getWriter()))
+  .then(download);
+}
+</script>

+ 0 - 13
src/options/views/tab-settings/vm-export/index.html

@@ -1,13 +0,0 @@
-<section>
-  <h3 v-text="i18n('labelDataExport')"></h3>
-  <select class=export-list multiple v-model="selectedIds">
-    <option class="ellipsis" v-for="script in store.scripts"
-      :value="script.id" v-text="script.custom.name||script.meta.name"></option>
-  </select>
-  <button v-text="i18n('buttonAllNone')" @click="updateSelection()"></button>
-  <button v-text="i18n('buttonExportData')" @click="exportData" :disabled="exporting"></button>
-  <label>
-    <input type=checkbox v-setting="'exportValues'">
-    <span v-text="i18n('labelExportScriptData')"></span>
-  </label>
-</section>

+ 0 - 132
src/options/views/tab-settings/vm-export/index.js

@@ -1,132 +0,0 @@
-var _ = require('src/common');
-var cache = require('src/cache');
-var utils = require('src/options/utils');
-var store = utils.store;
-
-module.exports = {
-  template: cache.get('./index.html'),
-  data: function () {
-    return {
-      store: store,
-      selectedIds: [],
-      exporting: false,
-    };
-  },
-  watch: {
-    'store.scripts': function () {
-      this.updateSelection(true);
-    },
-  },
-  created: function () {
-    this.updateSelection(true);
-  },
-  methods: {
-    updateSelection: function (select) {
-      var _this = this;
-      if (!store.scripts.length) return;
-      if (select == null) select = _this.selectedIds.length < store.scripts.length;
-      if (select) {
-        _this.selectedIds = store.scripts.map(function (script) {
-          return script.id;
-        });
-      } else {
-        _this.selectedIds = [];
-      }
-    },
-    exportData: function () {
-      var _this = this;
-      _this.exporting = true;
-      Promise.resolve(exportData(_this.selectedIds))
-      .catch(function (err) {
-        console.error(err);
-      })
-      .then(function () {
-        _this.exporting = false;
-      });
-    },
-  },
-};
-
-function getWriter() {
-  return new Promise(function (resolve, _reject) {
-    zip.createWriter(new zip.BlobWriter, function (writer) {
-      resolve(writer);
-    });
-  });
-}
-
-function addFile(writer, file) {
-  return new Promise(function (resolve, _reject) {
-    writer.add(file.name, new zip.TextReader(file.content), function () {
-      resolve(writer);
-    });
-  });
-}
-
-function download(writer) {
-  return new Promise(function (resolve, _reject) {
-    writer.close(function (blob) {
-      resolve(blob);
-    });
-  })
-  .then(function (blob) {
-    var url = URL.createObjectURL(blob);
-    var a = document.createElement('a');
-    a.href = url;
-    a.download = 'scripts.zip';
-    a.click();
-    setTimeout(function () {
-      URL.revokeObjectURL(url);
-    });
-  });
-}
-
-function exportData(selectedIds) {
-  if (!selectedIds.length) return;
-  var withValues = _.options.get('exportValues');
-  return _.sendMessage({
-    cmd: 'ExportZip',
-    data: {
-      values: withValues,
-      ids: selectedIds,
-    }
-  })
-  .then(function (data) {
-    var names = {};
-    var vm = {
-      scripts: {},
-      settings: _.options.get(),
-    };
-    if (withValues) vm.values = {};
-    var files = data.scripts.map(function (script) {
-      var name = script.custom.name || script.meta.name || 'Noname';
-      if (names[name]) name += '_' + (++ names[name]);
-      else names[name] = 1;
-      vm.scripts[name] = ['id', 'custom', 'enabled', 'update'].reduce(function (res, key) {
-        res[key] = script[key];
-        return res;
-      }, {});
-      if (withValues) {
-        var values = data.values[script.uri];
-        if (values) vm.values[script.uri] = values;
-      }
-      return {
-        name: name + '.user.js',
-        content: script.code,
-      };
-    });
-    files.push({
-      name: 'ViolentMonkey',
-      content: JSON.stringify(vm),
-    });
-    return files;
-  })
-  .then(function (files) {
-    return files.reduce(function (result, file) {
-      return result.then(function (writer) {
-        return addFile(writer, file);
-      });
-    }, getWriter());
-  })
-  .then(download);
-}

+ 137 - 0
src/options/views/tab-settings/vm-import.vue

@@ -0,0 +1,137 @@
+<template>
+  <section>
+    <h3 v-text="i18n('labelDataImport')"></h3>
+    <button v-text="i18n('buttonImportData')" @click="importFile"></button>
+    <button :title="i18n('hintVacuum')" @click="vacuum" :disabled="vacuuming" v-text="labelVacuum"></button>
+  </section>
+</template>
+
+<script>
+import { i18n, sendMessage } from 'src/common';
+import options from 'src/common/options';
+import { showMessage } from '../../utils';
+
+export default {
+  data() {
+    return {
+      vacuuming: false,
+      labelVacuum: this.i18n('buttonVacuum'),
+    };
+  },
+  methods: {
+    importFile() {
+      const input = document.createElement('input');
+      input.type = 'file';
+      input.accept = '.zip';
+      input.onchange = () => {
+        if (input.files && input.files.length) importData(input.files[0]);
+      };
+      input.click();
+    },
+    vacuum() {
+      this.vacuuming = true;
+      this.labelVacuum = this.i18n('buttonVacuuming');
+      sendMessage({ cmd: 'Vacuum' })
+      .then(() => {
+        this.vacuuming = false;
+        this.labelVacuum = this.i18n('buttonVacuumed');
+      });
+    },
+  },
+};
+
+function forEachItem(obj, cb) {
+  if (obj) {
+    Object.keys(obj).forEach((key) => {
+      cb(obj[key], key);
+    });
+  }
+}
+
+function getVMConfig(text) {
+  let vm;
+  try {
+    vm = JSON.parse(text);
+  } catch (e) {
+    console.warn('Error parsing ViolentMonkey configuration.');
+  }
+  vm = vm || {};
+  forEachItem(vm.values, (value, key) => {
+    if (value) {
+      sendMessage({
+        cmd: 'SetValue',
+        data: {
+          uri: key,
+          values: value,
+        },
+      });
+    }
+  });
+  forEachItem(vm.settings, (value, key) => {
+    options.set(key, value);
+  });
+  return vm;
+}
+
+function getVMFile(entry, vmFile) {
+  if (!entry.filename.endsWith('.user.js')) return;
+  const vm = vmFile || {};
+  return new Promise((resolve) => {
+    const writer = new zip.TextWriter();
+    entry.getData(writer, (text) => {
+      const script = { code: text };
+      if (vm.scripts) {
+        const more = vm.scripts[entry.filename.slice(0, -8)];
+        if (more) {
+          delete more.id;
+          script.more = more;
+        }
+      }
+      sendMessage({
+        cmd: 'ParseScript',
+        data: script,
+      }).then(() => resolve(true));
+    });
+  });
+}
+
+function getVMFiles(entries) {
+  const i = entries.findIndex(entry => entry.filename === 'ViolentMonkey');
+  if (i < 0) {
+    return { entries };
+  }
+  return new Promise((resolve) => {
+    const writer = new zip.TextWriter();
+    entries[i].getData(writer, (text) => {
+      entries.splice(i, 1);
+      resolve({
+        entries,
+        vm: getVMConfig(text),
+      });
+    });
+  });
+}
+
+function readZip(file) {
+  return new Promise((resolve, reject) => {
+    zip.createReader(new zip.BlobReader(file), (res) => {
+      res.getEntries((entries) => {
+        resolve(entries);
+      });
+    }, (err) => { reject(err); });
+  });
+}
+
+function importData(file) {
+  readZip(file)
+  .then(getVMFiles)
+  .then((data) => {
+    const { vm, entries } = data;
+    return Promise.all(entries.map(entry => getVMFile(entry, vm)));
+  })
+  .then(res => res.filter(Boolean).length)
+  .then((count) => {
+    showMessage({ text: i18n('msgImported', [count]) });
+  });
+}
+</script>

+ 0 - 5
src/options/views/tab-settings/vm-import/index.html

@@ -1,5 +0,0 @@
-<section>
-  <h3 v-text="i18n('labelDataImport')"></h3>
-  <button v-text="i18n('buttonImportData')" @click="importFile"></button>
-  <button :title="i18n('hintVacuum')" @click="vacuum" :disabled="vacuuming" v-text="labelVacuum"></button>
-</section>

+ 0 - 131
src/options/views/tab-settings/vm-import/index.js

@@ -1,131 +0,0 @@
-var _ = require('src/common');
-var cache = require('src/cache');
-var Message = require('src/options/views/message');
-
-module.exports = {
-  template: cache.get('./index.html'),
-  data: function () {
-    return {
-      vacuuming: false,
-      labelVacuum: _.i18n('buttonVacuum'),
-    };
-  },
-  methods: {
-    importFile: function () {
-      var input = document.createElement('input');
-      input.type = 'file';
-      input.accept = '.zip';
-      input.onchange = function () {
-        input.files && input.files.length && importData(input.files[0]);
-      };
-      input.click();
-    },
-    vacuum: function () {
-      var _this = this;
-      _this.vacuuming = true;
-      _this.labelVacuum = _.i18n('buttonVacuuming');
-      _.sendMessage({cmd: 'Vacuum'})
-      .then(function () {
-        _this.vacuuming = false;
-        _this.labelVacuum = _.i18n('buttonVacuumed');
-      });
-    },
-  },
-};
-
-function forEach(obj, cb) {
-  obj && Object.keys(obj).forEach(function (key) {
-    var value = obj[key];
-    cb(value, key);
-  });
-}
-
-function getVMConfig(text) {
-  var vm;
-  try {
-    vm = JSON.parse(text);
-  } catch (e) {
-    console.warn('Error parsing ViolentMonkey configuration.');
-  }
-  vm = vm || {};
-  forEach(vm.values, function (value, key) {
-    value && _.sendMessage({
-      cmd: 'SetValue',
-      data: {
-        uri: key,
-        values: value,
-      }
-    });
-  });
-  forEach(vm.settings, function (value, key) {
-    _.options.set(key, value);
-  });
-  return vm;
-}
-
-function getVMFile(entry, vm) {
-  if (!entry.filename.endsWith('.user.js')) return;
-  vm = vm || {};
-  return new Promise(function (resolve, _reject) {
-    var writer = new zip.TextWriter;
-    entry.getData(writer, function (text) {
-      var script = {code: text};
-      if (vm.scripts) {
-        var more = vm.scripts[entry.filename.slice(0, -8)];
-        if (more) {
-          delete more.id;
-          script.more = more;
-        }
-      }
-      _.sendMessage({
-        cmd: 'ParseScript',
-        data: script,
-      }).then(function () {
-        resolve(true);
-      });
-    });
-  });
-}
-
-function getVMFiles(entries) {
-  var i = entries.findIndex(function (entry) {
-    return entry.filename === 'ViolentMonkey';
-  });
-  if (~i) return new Promise(function (resolve, _reject) {
-    var writer = new zip.TextWriter;
-    entries[i].getData(writer, function (text) {
-      entries.splice(i, 1);
-      resolve({
-        vm: getVMConfig(text),
-        entries: entries,
-      });
-    });
-  });
-  return {
-    entries: entries,
-  };
-}
-
-function readZip(file) {
-  return new Promise(function (resolve, reject) {
-    zip.createReader(new zip.BlobReader(file), function (res) {
-      res.getEntries(function (entries) {
-        resolve(entries);
-      });
-    }, function (err) {reject(err);});
-  });
-}
-
-function importData(file) {
-  readZip(file).then(getVMFiles).then(function (data) {
-    var vm = data.vm;
-    var entries = data.entries;
-    return Promise.all(entries.map(function (entry) {
-      return getVMFile(entry, vm);
-    })).then(function (res) {
-      return res.filter(function (item) {return item;}).length;
-    });
-  }).then(function (count) {
-    Message.open({text: _.i18n('msgImported', [count])});
-  });
-}

+ 129 - 0
src/options/views/tab-settings/vm-sync.vue

@@ -0,0 +1,129 @@
+<template>
+  <section v-feature="'sync'">
+    <h3>
+      <span class="feature-text" v-text="i18n('labelSync')"></span>
+    </h3>
+    <div>
+      <span v-text="i18n('labelSyncService')"></span>
+      <select :value="syncConfig.current" @change="onSyncChange">
+        <option v-for="service in syncServices" v-text="service.displayName" :value="service.name"></option>
+      </select>
+      <button v-text="labelAuthorize" v-if="service.name"
+      :disabled="!canAuthorize" @click="onAuthorize"></button>
+      <button :disabled="!canSync" v-if="service.name" @click="onSync">
+        <svg class="icon"><use xlink:href="#refresh" /></svg>
+      </button>
+    </div>
+    <p class="mt-1" v-text="message"></p>
+    <h4 v-text="i18n('labelSyncSettings')"></h4>
+    <div class="mt-1">
+      <label>
+        <input type="checkbox" v-setting="'syncScriptStatus'">
+        <span v-text="i18n('labelSyncScriptStatus')"></span>
+      </label>
+    </div>
+  </section>
+</template>
+
+<script>
+import { sendMessage } from 'src/common';
+import options from 'src/common/options';
+import { store } from '../../utils';
+
+const SYNC_CURRENT = 'sync.current';
+const syncConfig = {
+  current: '',
+};
+options.hook((data) => {
+  if (SYNC_CURRENT in data) {
+    syncConfig.current = data[SYNC_CURRENT] || '';
+  }
+});
+
+export default {
+  data() {
+    return {
+      syncConfig,
+      store,
+    };
+  },
+  computed: {
+    syncServices() {
+      let services = [{
+        displayName: this.i18n('labelSyncDisabled'),
+        name: '',
+      }];
+      const states = this.store.sync;
+      if (states && states.length) {
+        services = services.concat(states);
+        this.$nextTick(() => {
+          // Set `current` after options are ready
+          syncConfig.current = options.get(SYNC_CURRENT);
+        });
+      }
+      return services;
+    },
+    service() {
+      const current = this.syncConfig.current || '';
+      let service = this.syncServices.find(item => item.name === current);
+      if (!service) {
+        console.warn('Invalid current service:', current);
+        service = this.syncServices[0];
+      }
+      return service;
+    },
+    message() {
+      const { service } = this;
+      if (service.authState === 'initializing') return this.i18n('msgSyncInit');
+      if (service.authState === 'error') return this.i18n('msgSyncInitError');
+      if (service.syncState === 'error') return this.i18n('msgSyncError');
+      if (service.syncState === 'ready') return this.i18n('msgSyncReady');
+      if (service.syncState === 'syncing') {
+        let progress = '';
+        if (service.progress && service.progress.total) {
+          progress = ` (${service.progress.finished}/${service.progress.total})`;
+        }
+        return this.i18n('msgSyncing') + progress;
+      }
+      if (service.lastSync) {
+        const lastSync = new Date(service.lastSync).toLocaleString();
+        return this.i18n('lastSync', lastSync);
+      }
+    },
+    labelAuthorize() {
+      const { service } = this;
+      if (service.authState === 'authorizing') return this.i18n('labelSyncAuthorizing');
+      if (service.authState === 'authorized') return this.i18n('labelSyncRevoke');
+      return this.i18n('labelSyncAuthorize');
+    },
+    canAuthorize() {
+      const { service } = this;
+      return ['unauthorized', 'error', 'authorized'].includes(service.authState)
+      && ['idle', 'error'].includes(service.syncState);
+    },
+    canSync() {
+      const { service } = this;
+      return this.canAuthorize && service.authState === 'authorized';
+    },
+  },
+  methods: {
+    onSyncChange(e) {
+      const { value } = e.target;
+      options.set(SYNC_CURRENT, value);
+    },
+    onAuthorize() {
+      const { service } = this;
+      if (['authorized'].includes(service.authState)) {
+        // revoke
+        sendMessage({ cmd: 'SyncRevoke' });
+      } else if (['unauthorized', 'error'].includes(service.authState)) {
+        // authorize
+        sendMessage({ cmd: 'SyncAuthorize' });
+      }
+    },
+    onSync() {
+      sendMessage({ cmd: 'SyncStart' });
+    },
+  },
+};
+</script>

+ 0 - 24
src/options/views/tab-settings/vm-sync/index.html

@@ -1,24 +0,0 @@
-<section v-feature="'sync'">
-  <h3>
-    <span class="feature-text" v-text="i18n('labelSync')"></span>
-  </h3>
-  <div>
-    <span v-text="i18n('labelSyncService')"></span>
-    <select :value="syncConfig.current" @change="onSyncChange">
-      <option v-for="service in syncServices" v-text="service.displayName" :value="service.name"></option>
-    </select>
-    <button v-text="labelAuthorize" v-if="service.name"
-    :disabled="!canAuthorize" @click="onAuthorize"></button>
-    <button :disabled="!canSync" v-if="service.name" @click="onSync">
-      <svg class="icon"><use xlink:href="#refresh" /></svg>
-    </button>
-  </div>
-  <p class="mt-1" v-text="message"></p>
-  <h4 v-text="i18n('labelSyncSettings')"></h4>
-  <div class="mt-1">
-    <label>
-      <input type="checkbox" v-setting="'syncScriptStatus'">
-      <span v-text="i18n('labelSyncScriptStatus')"></span>
-    </label>
-  </div>
-</section>

+ 0 - 116
src/options/views/tab-settings/vm-sync/index.js

@@ -1,116 +0,0 @@
-var _ = require('src/common');
-var cache = require('src/cache');
-var utils = require('src/options/utils');
-var store = utils.store;
-
-var SYNC_CURRENT = 'sync.current';
-var syncConfig = {
-  current: '',
-};
-_.options.hook(function (data) {
-  if (SYNC_CURRENT in data) {
-    syncConfig.current = data[SYNC_CURRENT] || '';
-  }
-});
-
-module.exports = {
-  template: cache.get('./index.html'),
-  data: function () {
-    return {
-      syncConfig: syncConfig,
-      store: store,
-    };
-  },
-  computed: {
-    syncServices: function () {
-      var services = [{
-        displayName: _.i18n('labelSyncDisabled'),
-        name: '',
-      }];
-      var states = this.store.sync;
-      if (states && states.length) {
-        services = services.concat(states);
-        this.$nextTick(function () {
-          // Set `current` after options are ready
-          syncConfig.current = _.options.get(SYNC_CURRENT);
-        });
-      }
-      return services;
-    },
-    service: function () {
-      var current = this.syncConfig.current || '';
-      var service = this.syncServices.find(function (item) {
-        return item.name === current;
-      });
-      if (!service) {
-        console.warn('Invalid current service:', current);
-        service = this.syncServices[0];
-      }
-      return service;
-    },
-    message: function () {
-      var service = this.service;
-      if (service.authState === 'initializing') return _.i18n('msgSyncInit');
-      if (service.authState === 'error') return _.i18n('msgSyncInitError');
-      if (service.syncState === 'error') return _.i18n('msgSyncError');
-      if (service.syncState === 'ready') return _.i18n('msgSyncReady');
-      if (service.syncState === 'syncing') {
-        var progress = '';
-        if (service.progress && service.progress.total) {
-          progress = ' (' + service.progress.finished + '/' + service.progress.total + ')';
-        }
-        return _.i18n('msgSyncing') + progress;
-      }
-      if (service.lastSync) {
-        var lastSync = new Date(service.lastSync).toLocaleString();
-        return _.i18n('lastSync', lastSync);
-      }
-    },
-    labelAuthorize: function () {
-      var service = this.service;
-      if (service.authState === 'authorizing') return _.i18n('labelSyncAuthorizing');
-      if (service.authState === 'authorized') return _.i18n('labelSyncRevoke');
-      return _.i18n('labelSyncAuthorize');
-    },
-    canAuthorize: function () {
-      var service = this.service;
-      return ~['unauthorized', 'error', 'authorized'].indexOf(service.authState)
-      && ~['idle', 'error'].indexOf(service.syncState);
-    },
-    canSync: function () {
-      var service = this.service;
-      return this.canAuthorize && service.authState === 'authorized';
-    },
-  },
-  methods: {
-    onEnableService: function (name) {
-      store.sync.forEach(function (service) {
-        if (service.name !== name) {
-          var key = service.name + '.enabled';
-          var enabled = _.options.get(key);
-          if (enabled) {
-            _.options.set(key, false);
-          }
-        }
-      });
-      _.sendMessage({cmd: 'SyncStart'});
-    },
-    onSyncChange: function (e) {
-      var value = e.target.value;
-      _.options.set(SYNC_CURRENT, value);
-    },
-    onAuthorize: function () {
-      var service = this.service;
-      if (~['authorized'].indexOf(service.authState)) {
-        // revoke
-        _.sendMessage({cmd: 'SyncRevoke'});
-      } else if (~['unauthorized', 'error'].indexOf(service.authState)) {
-        // authorize
-        _.sendMessage({cmd: 'SyncAuthorize'});
-      }
-    },
-    onSync: function () {
-      _.sendMessage({cmd: 'SyncStart'});
-    },
-  },
-};

+ 0 - 21
src/public/mylib/CodeMirror/fold.css

@@ -1,21 +0,0 @@
-.CodeMirror-foldmarker {
-	color: blue;
-	text-shadow: #b9f 1px 1px 2px, #b9f -1px -1px 2px, #b9f 1px -1px 2px, #b9f -1px 1px 2px;
-	font-family: arial;
-	line-height: .3;
-	cursor: pointer;
-}
-.CodeMirror-foldgutter {
-	width: .7em;
-}
-.CodeMirror-foldgutter-open,
-.CodeMirror-foldgutter-folded {
-	color: #555;
-	cursor: pointer;
-}
-.CodeMirror-foldgutter-open:after {
-	content: "\25BE";
-}
-.CodeMirror-foldgutter-folded:after {
-	content: "\25B8";
-}