Просмотр исходного кода

Merge pull request #866 from weseek/support/use-eslint-autofixonsave

Support/use eslint autofixonsave
Yuki Takei 7 лет назад
Родитель
Сommit
a400560799
100 измененных файлов с 1710 добавлено и 1414 удалено
  1. 1 0
      .eslintignore
  2. 108 76
      .eslintrc.js
  3. 1 0
      .prettierignore
  4. 5 0
      .prettierrc
  5. 19 0
      .stylelintrc.json
  6. 1 1
      .vscode/extensions.json
  7. 12 0
      .vscode/settings.json
  8. 1 1
      bin/download-cdn-resources.js
  9. 2 2
      bin/generate-plugin-definitions-source.js
  10. 5 4
      bin/shrink-emojione-strategy.js
  11. 6 2
      config/logger/config.dev.js
  12. 6 6
      config/migrate.js
  13. 20 20
      config/webpack.common.js
  14. 3 3
      config/webpack.dev.dll.js
  15. 8 8
      config/webpack.dev.js
  16. 21 15
      config/webpack.prod.js
  17. 13 3
      package.json
  18. 36 36
      resource/cdn-manifests.js
  19. 125 87
      src/client/js/app.js
  20. 6 9
      src/client/js/components/Admin/CustomCssEditor.js
  21. 6 9
      src/client/js/components/Admin/CustomHeaderEditor.js
  22. 6 9
      src/client/js/components/Admin/CustomScriptEditor.js
  23. 1 2
      src/client/js/components/Common/UserDate.js
  24. 15 7
      src/client/js/components/CopyButton.js
  25. 4 3
      src/client/js/components/Page/PagePath.js
  26. 6 4
      src/client/js/components/Page/RevisionBody.js
  27. 23 16
      src/client/js/components/Page/RevisionPath.js
  28. 12 6
      src/client/js/components/Page/RevisionUrl.js
  29. 16 11
      src/client/js/components/PageAttachment.js
  30. 10 5
      src/client/js/components/PageAttachment/Attachment.js
  31. 11 5
      src/client/js/components/PageAttachment/DeleteAttachmentModal.js
  32. 7 6
      src/client/js/components/PageAttachment/PageAttachmentList.js
  33. 22 20
      src/client/js/components/PageComment/Comment.js
  34. 7 8
      src/client/js/components/PageComment/CommentPreview.js
  35. 2 6
      src/client/js/components/PageComment/DeleteCommentModal.js
  36. 34 26
      src/client/js/components/PageComments.js
  37. 30 23
      src/client/js/components/PageEditor.js
  38. 2 2
      src/client/js/components/PageEditor/AbstractEditor.js
  39. 20 16
      src/client/js/components/PageEditor/Cheatsheet.js
  40. 174 120
      src/client/js/components/PageEditor/CodeMirrorEditor.js
  41. 17 17
      src/client/js/components/PageEditor/EmojiAutoCompleteHelper.js
  42. 1 1
      src/client/js/components/PageEditor/MarkdownListUtil.js
  43. 3 6
      src/client/js/components/PageEditor/MarkdownTableInterceptor.js
  44. 13 13
      src/client/js/components/PageEditor/MarkdownTableUtil.js
  45. 68 42
      src/client/js/components/PageEditor/OptionsSelector.js
  46. 3 1
      src/client/js/components/PageEditor/PasteHelper.js
  47. 3 6
      src/client/js/components/PageEditor/PreventMarkdownListInterceptor.js
  48. 8 9
      src/client/js/components/PageEditor/Preview.js
  49. 38 36
      src/client/js/components/PageEditor/ScrollSyncHelper.js
  50. 10 8
      src/client/js/components/PageEditor/SimpleCheatsheet.js
  51. 23 18
      src/client/js/components/PageEditor/TextAreaEditor.js
  52. 62 57
      src/client/js/components/PageHistory.js
  53. 8 5
      src/client/js/components/PageHistory/RevisionDiff.js
  54. 3 3
      src/client/js/components/PageList/ListView.js
  55. 3 4
      src/client/js/components/PageList/Page.js
  56. 2 3
      src/client/js/components/PageList/PageListMeta.js
  57. 4 6
      src/client/js/components/PageList/PagePath.js
  58. 6 6
      src/client/js/components/ReactUtils.js
  59. 5 4
      src/client/js/components/SearchForm.js
  60. 31 32
      src/client/js/components/SearchPage.js
  61. 4 8
      src/client/js/components/SearchPage/DeletePageListModal.js
  62. 23 19
      src/client/js/components/SearchPage/SearchPageForm.js
  63. 88 70
      src/client/js/components/SearchPage/SearchResult.js
  64. 3 4
      src/client/js/components/SearchPage/SearchResultList.js
  65. 25 27
      src/client/js/components/SearchTypeahead.js
  66. 6 7
      src/client/js/components/User/User.js
  67. 9 9
      src/client/js/components/User/UserPicture.js
  68. 20 19
      src/client/js/hackmd-agent.js
  69. 1 1
      src/client/js/hackmd-styles.js
  70. 2 2
      src/client/js/i18n.js
  71. 1 1
      src/client/js/installer.js
  72. 19 17
      src/client/js/legacy/crowi-admin.js
  73. 4 4
      src/client/js/legacy/crowi-presentation.js
  74. 136 135
      src/client/js/legacy/crowi.js
  75. 14 13
      src/client/js/models/MarkdownTable.js
  76. 1 1
      src/client/js/plugin.js
  77. 44 44
      src/client/js/util/Crowi.js
  78. 17 14
      src/client/js/util/GrowiRenderer.js
  79. 13 13
      src/client/js/util/PostProcessor/CrowiTemplate.js
  80. 2 1
      src/client/js/util/PreProcessor/CsvToTable.js
  81. 3 3
      src/client/js/util/PreProcessor/Linker.js
  82. 4 5
      src/client/js/util/PreProcessor/XssFilter.js
  83. 1 1
      src/client/js/util/codemirror/update-display-util.ext.js
  84. 14 11
      src/client/js/util/interceptor/detach-code-blocks.js
  85. 1 0
      src/client/js/util/markdown-it/blockdiag.js
  86. 4 2
      src/client/js/util/markdown-it/emoji.js
  87. 3 3
      src/client/js/util/markdown-it/header-line-number.js
  88. 1 0
      src/client/js/util/markdown-it/header-with-edit-link.js
  89. 3 3
      src/client/js/util/markdown-it/header.js
  90. 1 1
      src/client/js/util/markdown-it/mathjax.js
  91. 1 0
      src/client/js/util/markdown-it/plantuml.js
  92. 3 1
      src/client/js/util/markdown-it/table-with-handsontable-button.js
  93. 37 34
      src/client/js/util/reveal/plugins/growi-renderer.js
  94. 15 16
      src/client/styles/scss/_admin.scss
  95. 8 7
      src/client/styles/scss/_attachments.scss
  96. 5 6
      src/client/styles/scss/_comment.scss
  97. 14 15
      src/client/styles/scss/_comment_growi.scss
  98. 18 19
      src/client/styles/scss/_comment_kibela.scss
  99. 8 8
      src/client/styles/scss/_create-page.scss
  100. 16 16
      src/client/styles/scss/_editor-attachment.scss

+ 1 - 0
.eslintignore

@@ -3,5 +3,6 @@
 /node_modules/**
 /public/**
 /src/client/js/legacy/thirdparty-js/**
+/src/client/js/util/reveal/plugins/markdown.js
 /test/**
 /tmp/**

+ 108 - 76
.eslintrc.js

@@ -1,93 +1,125 @@
 module.exports = {
-  "env": {
-    "browser": true,
-    "commonjs": true,
-    "es6": true,
-    "node": true
-  },
-  "extends": [
-    "eslint:recommended",
-    "plugin:react/recommended"
+  parser: 'babel-eslint',
+  extends: [
+    'airbnb',
+    'plugin:react/recommended',
   ],
-  "globals": {
-    "$": true,
-    "jquery": true,
-    "emojione": true,
-    "hljs": true,
-    "window": true
+  env: {
+    browser: true,
+    node: true,
+    es6: true,
+    mocha: true,
+    jquery: true,
   },
-  "parserOptions": {
-    "ecmaVersion": 8,
-    "ecmaFeatures": {
-      "experimentalObjectRestSpread": true,
-      "jsx": true
-    },
-    "sourceType": "module"
+  globals: {
+    $: true,
+    jquery: true,
+    emojione: true,
+    hljs: true,
+    window: true,
   },
-  "plugins": [
-    "react"
+  plugins: [
+    'react',
+    'chai-friendly',
   ],
-  "rules": {
-    "brace-style": [
-      "error",
-      "stroustrup", { "allowSingleLine": true }
-    ],
-    "comma-spacing": [
-      "error",
-      { "before": false, "after": true }
+  rules: {
+    'arrow-body-style': ['error', 'always'],
+    'brace-style': [
+      'error',
+      'stroustrup',
+      { allowSingleLine: true },
     ],
-    "func-call-spacing": [
-      "error",
-      "never"
+    'class-methods-use-this': 'off',
+    'consistent-return': 'off',
+    'default-case': 'off',
+    'func-names': 'off',
+    'global-require': 'off',
+    'key-spacing': [
+      'error',
+      { mode: 'minimum' },
     ],
-    "indent": [
-      "error",
-      2,
+    'max-len': ['error',
       {
-        "SwitchCase": 1,
-        "ignoredNodes": ['JSXElement *', 'JSXElement', "JSXAttribute", "JSXSpreadAttribute"],
-        "FunctionDeclaration": {"body": 1, "parameters": 2},
-        "FunctionExpression": {"body": 1, "parameters": 2},
-        "MemberExpression": "off"
-      }
+        code: 160,
+        ignoreTrailingComments: true,
+      },
     ],
-    "key-spacing": [
-      "error", {
-        "beforeColon": false,
-        "afterColon": true,
-        "mode": "minimum"
-      }
+    'no-continue': 'off',
+    'no-param-reassign': [
+      'error',
+      { props: false },
+    ],
+    'no-plusplus': 'off',
+    // Allow only for-of
+    // https://qiita.com/the_red/items/0c826e97b57da6d67621
+    'no-restricted-syntax': [
+      'error',
+      {
+        selector: 'ForInStatement',
+        message: 'for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.',
+      },
+      {
+        selector: 'LabeledStatement',
+        message: 'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.',
+      },
+      {
+        selector: 'WithStatement',
+        message: '`with` is disallowed in strict mode because it makes code impossible to predict and optimize.',
+      },
     ],
-    "keyword-spacing": [
-      "error", {}
+    'no-shadow': 'off',
+    'no-underscore-dangle': 'off',
+    'no-useless-return': 'off',
+    'prefer-destructuring': 'off',
+    'indent': [
+      'error',
+      2,
+      {
+        SwitchCase: 1,
+        ignoredNodes: ['JSXElement *', 'JSXElement', 'JSXAttribute', 'JSXSpreadAttribute'],
+        ArrayExpression: 'first',
+        FunctionDeclaration: { body: 1, parameters: 2 },
+        FunctionExpression: { body: 1, parameters: 2 },
+      },
     ],
-    "linebreak-style": [
-      "error",
-      "unix"
+    'no-unused-vars': [
+      'error',
+      { args: 'none' },
     ],
-    "no-unused-vars": [
-      "error",
-      { "args": "none" }
+    'padded-blocks': [
+      'error',
+      { classes: 'always' },
     ],
-    "no-var": [ "error" ],
-    "quotes": [
-      "error",
-      "single"
+    'radix': 'off',
+    'semi': [
+      'error',
+      'always',
+      { omitLastInOneLineBlock: true },
     ],
-    "react/jsx-uses-vars": 1,
-    "react/no-string-refs": "off",
-    "semi": [
-      "error",
-      "always",
-      { "omitLastInOneLineBlock": true }
+    'space-before-function-paren': [
+      'error',
+      'never',
     ],
-    "space-before-blocks": [
-      "error",
-      "always"
+    'react/destructuring-assignment': 'off',
+    'react/forbid-prop-types': 'off',
+    'react/jsx-filename-extension': [
+      'warn',
+      { extensions: ['.jsx']},
     ],
-    "space-before-function-paren": [
-      "error",
-      "never"
-    ]
-  }
+    'react/no-unused-prop-types': 'off',
+    'react/jsx-one-expression-per-line': 'off',
+    'react/no-access-state-in-setstate': 'off',
+    'react/prefer-stateless-function': 'off',
+    'react/require-default-props': 'off',
+    'react/self-closing-comp': 'off',
+    'react/sort-comp': 'off',
+    'jsx-a11y/img-redundant-alt': 'off',
+    // eslint-plugin-import rules
+    'import/no-extraneous-dependencies': 'off',
+    'import/no-dynamic-require': 'off',
+    'import/no-unresolved': [2, { ignore: ['^@'] }], // ignore @alias/..., @commons/..., ...
+    // eslint-plugin-chai-friendly rules
+    'no-unused-expressions': 0,
+    'chai-friendly/no-unused-expressions': 2,
+  },
 };

+ 1 - 0
.prettierignore

@@ -0,0 +1 @@
+src/client/styles/scss/_override-bootstrap-variables.scss

+ 5 - 0
.prettierrc

@@ -0,0 +1,5 @@
+{
+  "printWidth": 160,
+  "singleQuote": true,
+  "trailingComma": "all"
+}

+ 19 - 0
.stylelintrc.json

@@ -0,0 +1,19 @@
+{
+  "extends": [
+    "stylelint-config-recess-order",
+    "./node_modules/prettier-stylelint/config.js"
+  ],
+  "ignoreFiles": [
+    "src/client/styles/scss/_override-bootstrap-variables.scss"
+  ],
+  "rules": {
+    "indentation": 2,
+    "string-quotes": "single",
+    "rule-empty-line-before": [ "always-multi-line", {
+      "except": ["after-single-line-comment", "first-nested"],
+      "ignore": ["after-comment", "inside-block"]
+    } ],
+    "selector-combinator-space-before": "always",
+    "selector-combinator-space-after": "always"
+  }
+}

+ 1 - 1
.vscode/extensions.json

@@ -4,13 +4,13 @@
 
 	// List of extensions which should be recommended for users of this workspace.
 	"recommendations": [
-    "hookyqr.beautify",
     "msjsdiag.debugger-for-chrome",
     "hbenl.vscode-firefox-debug",
     "editorconfig.editorconfig",
     "dbaeumer.vscode-eslint",
     "eg2.vscode-npm-script",
     "christian-kohler.npm-intellisense",
+    "esbenp.prettier-vscode",
 	],
 	// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
 	"unwantedRecommendations": [

+ 12 - 0
.vscode/settings.json

@@ -4,5 +4,17 @@
   // 指定した構文に対してプロファイルを定義するか、特定の規則がある独自のプロファイルをご使用ください。
   "emmet.syntaxProfiles": {
     "javascript": "jsx"
+  },
+
+  // for vscode-eslint
+  "eslint.autoFixOnSave": true,
+  "[javascript]": {
+    "editor.formatOnSave": false
+  },
+
+  // for prettier-vecode + prettier-stylelint
+  "prettier.stylelintIntegration": true,
+  "[scss]": {
+    "editor.formatOnSave": true
   }
 }

+ 1 - 1
bin/download-cdn-resources.js

@@ -27,6 +27,6 @@ service.downloadAndWriteAll(downloader)
   .then(() => {
     logger.info('Download is terminated successfully');
   })
-  .catch(err => {
+  .catch((err) => {
     logger.error(err);
   });

+ 2 - 2
bin/generate-plugin-definitions-source.js

@@ -11,6 +11,7 @@ const swig = require('swig-templates');
 
 const helpers = require('@commons/util/helpers');
 const PluginUtils = require('../src/server/plugins/plugin-utils');
+
 const pluginUtils = new PluginUtils();
 
 const TEMPLATE = helpers.root('bin/templates/plugin-definitions.js.swig');
@@ -23,7 +24,6 @@ let pluginNames = pluginUtils.listPluginNames(helpers.root());
 if (process.env.NODE_ENV === 'development'
     && process.env.PLUGIN_NAMES_TOBE_LOADED !== undefined
     && process.env.PLUGIN_NAMES_TOBE_LOADED.length > 0) {
-
   const pluginNamesDev = process.env.PLUGIN_NAMES_TOBE_LOADED.split(',');
 
   // merge and remove duplicates
@@ -48,7 +48,7 @@ const definitions = pluginNames
   });
 
 const compiledTemplate = swig.compileFile(TEMPLATE);
-const code = compiledTemplate({definitions});
+const code = compiledTemplate({ definitions });
 
 // write
 fs.writeFileSync(OUT, code);

+ 5 - 4
bin/shrink-emojione-strategy.js

@@ -11,21 +11,22 @@ const helpers = require('@commons/util/helpers');
 
 const emojiStrategy = require('emojione/emoji_strategy.json');
 const markdownItEmojiFull = require('markdown-it-emoji/lib/data/full.json');
+
 const OUT = helpers.root('tmp/emoji_strategy_shrinked.json');
 
-let shrinkedMap = {};
-for (let unicode in emojiStrategy) {
+const shrinkedMap = {};
+Object.keys(emojiStrategy).forEach((unicode) => {
   const data = emojiStrategy[unicode];
   const shortname = data.shortname.replace(/:/g, '');
 
   // ignore if it isn't included in markdownItEmojiFull
   if (markdownItEmojiFull[shortname] == null) {
-    continue;
+    return;
   }
 
   // add
   shrinkedMap[unicode] = data;
-}
+});
 
 // write
 fs.writeFileSync(OUT, JSON.stringify(shrinkedMap));

+ 6 - 2
config/logger/config.dev.js

@@ -1,7 +1,9 @@
 module.exports = {
   default: 'info',
 
-  //// configure level for server
+  /*
+   * configure level for server
+   */
   // 'express:*': 'debug',
   // 'growi:*': 'debug',
   'growi:crowi': 'debug',
@@ -21,6 +23,8 @@ module.exports = {
   // email
   // 'growi:lib:mailer': 'debug',
 
-  //// configure level for client
+  /*
+   * configure level for client
+   */
   'growi:app': 'debug',
 };

+ 6 - 6
config/migrate.js

@@ -6,11 +6,11 @@
  */
 
 function getMongoUri(env) {
-  return env.MONGOLAB_URI || // for B.C.
-    env.MONGODB_URI || // MONGOLAB changes their env name
-    env.MONGOHQ_URL ||
-    env.MONGO_URI ||
-    ((env.NODE_ENV === 'test') ? 'mongodb://localhost/growi_test' : 'mongodb://localhost/growi');
+  return env.MONGOLAB_URI // for B.C.
+    || env.MONGODB_URI // MONGOLAB changes their env name
+    || env.MONGOHQ_URL
+    || env.MONGO_URI
+    || ((env.NODE_ENV === 'test') ? 'mongodb://localhost/growi_test' : 'mongodb://localhost/growi');
 }
 
 const mongoUri = getMongoUri(process.env);
@@ -25,5 +25,5 @@ module.exports = {
     },
   },
   migrationsDir: 'src/migrations/',
-  changelogCollectionName: 'migrations'
+  changelogCollectionName: 'migrations',
 };

+ 20 - 20
config/webpack.common.js

@@ -2,13 +2,13 @@
  * @author: Yuki Takei <yuki@weseek.co.jp>
  */
 const webpack = require('webpack');
-const helpers = require('../src/lib/util/helpers');
 
 /*
  * Webpack Plugins
  */
 const WebpackAssetsManifest = require('webpack-assets-manifest');
 const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
+const helpers = require('../src/lib/util/helpers');
 
 /*
  * Webpack configuration
@@ -45,7 +45,7 @@ module.exports = (options) => {
       'styles/theme-island':      './src/client/styles/scss/theme/island.scss',
       // styles for external services
       'styles/style-hackmd':          './src/client/styles/hackmd/style.scss',
-    }, options.entry || {}),  // Merge with env dependent settings
+    }, options.entry || {}), // Merge with env dependent settings
     output: Object.assign({
       path: helpers.root('public'),
       publicPath: '/',
@@ -54,9 +54,9 @@ module.exports = (options) => {
     externals: {
       // require("jquery") is external and available
       //  on the global var jQuery
-      'jquery': 'jQuery',
-      'emojione': 'emojione',
-      'hljs': 'hljs',
+      jquery: 'jQuery',
+      emojione: 'emojione',
+      hljs: 'hljs',
     },
     resolve: {
       extensions: ['.js', '.jsx', '.json'],
@@ -69,8 +69,8 @@ module.exports = (options) => {
         '@alias/logger': helpers.root('src/lib/service/logger'),
         '@alias/locales': helpers.root('resource/locales'),
         // replace bunyan
-        'bunyan': 'browser-bunyan',
-      }
+        bunyan: 'browser-bunyan',
+      },
     },
     module: {
       rules: options.module.rules.concat([
@@ -78,27 +78,27 @@ module.exports = (options) => {
           test: /.jsx?$/,
           exclude: {
             test:    helpers.root('node_modules'),
-            exclude: [  // include as a result
+            exclude: [ // include as a result
               { test: helpers.root('node_modules', 'growi-plugin-') },
               helpers.root('node_modules/codemirror/src'),
               helpers.root('node_modules/string-width'),
               helpers.root('node_modules/is-fullwidth-code-point'), // depends from string-width
-            ]
+            ],
           },
           use: [{
-            loader: 'babel-loader?cacheDirectory'
-          }]
+            loader: 'babel-loader?cacheDirectory',
+          }],
         },
         {
           test: /locales/,
           loader: '@alienfast/i18next-loader',
           options: {
             basenameAsNamespace: true,
-          }
+          },
         },
         { // see https://github.com/abpetkov/switchery/issues/120
           test: /switchery\.js$/,
-          loader: 'imports-loader?module=>false,exports=>false,define=>false,this=>window'
+          loader: 'imports-loader?module=>false,exports=>false,define=>false,this=>window',
         },
         /*
          * File loader for supporting images, for example, in CSS files.
@@ -112,8 +112,8 @@ module.exports = (options) => {
         {
           test: /\.(eot|woff2?|svg|ttf)([?]?.*)$/,
           use: 'null-loader',
-        }
-      ])
+        },
+      ]),
     },
     plugins: options.plugins.concat([
 
@@ -127,7 +127,7 @@ module.exports = (options) => {
       new webpack.IgnorePlugin(/^\.\/lib\/deflate\.js/, /markdown-it-plantuml/),
 
       new LodashModuleReplacementPlugin({
-        flattening: true
+        flattening: true,
       }),
 
       new webpack.ProvidePlugin({ // refs externals
@@ -152,7 +152,7 @@ module.exports = (options) => {
             name: 'styles/style-commons',
             minSize: 1,
             priority: 30,
-            enforce: true
+            enforce: true,
           },
           commons: {
             test: /src[\\/].*\.jsx?$/,
@@ -160,7 +160,7 @@ module.exports = (options) => {
             name: 'js/commons',
             minChunks: 2,
             minSize: 1,
-            priority: 20
+            priority: 20,
           },
           vendors: {
             test: /node_modules[\\/].*\.jsx?$/,
@@ -171,9 +171,9 @@ module.exports = (options) => {
             name: 'js/vendors',
             minSize: 1,
             priority: 10,
-            enforce: true
+            enforce: true,
           },
-        }
+        },
       },
       minimizer: options.optimization.minimizer || [],
     },

+ 3 - 3
config/webpack.dev.dll.js

@@ -30,7 +30,7 @@ module.exports = {
       'xss',
       // GROWI Libraries
       'growi-pluginkit',
-    ]
+    ],
   },
   output: {
     path: helpers.root('public/dll'),
@@ -44,7 +44,7 @@ module.exports = {
   plugins: [
     new webpack.DllPlugin({
       path: helpers.root('public/dll/manifest.json'),
-      name: 'growi_dlls'
-    })
+      name: 'growi_dlls',
+    }),
   ],
 };

+ 8 - 8
config/webpack.dev.js

@@ -3,18 +3,18 @@
  */
 
 const webpack = require('webpack');
-const helpers = require('../src/lib/util/helpers');
 
 /*
  * Webpack Plugins
  */
 const MiniCssExtractPlugin = require('mini-css-extract-plugin');
-const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
+const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
+const helpers = require('../src/lib/util/helpers');
 
 /**
  * Webpack Constants
  */
-const ANALYZE = process.env.ANALYZE;
+const { ANALYZE } = process.env;
 
 module.exports = require('./webpack.common')({
   mode: 'development',
@@ -37,19 +37,19 @@ module.exports = require('./webpack.common')({
         exclude: [
           helpers.root('src/client/styles/hackmd'),
           helpers.root('src/client/styles/scss/style-presentation.scss'),
-        ]
+        ],
       },
       { // Dump CSS for HackMD
         test: /\.(css|scss)$/,
         use: [
           MiniCssExtractPlugin.loader,
           'css-loader',
-          'sass-loader'
+          'sass-loader',
         ],
         include: [
           helpers.root('src/client/styles/hackmd'),
           helpers.root('src/client/styles/scss/style-presentation.scss'),
-        ]
+        ],
       },
     ],
   },
@@ -71,7 +71,7 @@ module.exports = require('./webpack.common')({
   ],
   optimization: {},
   performance: {
-    hints: false
-  }
+    hints: false,
+  },
 
 });

+ 21 - 15
config/webpack.prod.js

@@ -1,7 +1,6 @@
 /**
  * @author: Yuki Takei <yuki@weseek.co.jp>
  */
-const helpers = require('../src/lib/util/helpers');
 
 /**
  * Webpack Plugins
@@ -9,19 +8,21 @@ const helpers = require('../src/lib/util/helpers');
 const TerserPlugin = require('terser-webpack-plugin');
 const MiniCssExtractPlugin = require('mini-css-extract-plugin');
 const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
-const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
+const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
+
+const helpers = require('../src/lib/util/helpers');
 
 /**
  * Webpack Constants
  */
-const ANALYZE = process.env.ANALYZE;
+const { ANALYZE } = process.env;
 
 module.exports = require('./webpack.common')({
   mode: 'production',
   devtool: undefined,
   output: {
     filename: '[name].[chunkhash].bundle.js',
-    chunkFilename: '[name].[chunkhash].chunk.js'
+    chunkFilename: '[name].[chunkhash].chunk.js',
   },
   module: {
     rules: [
@@ -30,22 +31,27 @@ module.exports = require('./webpack.common')({
         use: [
           MiniCssExtractPlugin.loader,
           'css-loader',
-          { loader: 'postcss-loader', options: {
-            sourceMap: false,
-            plugins: (loader) => [
-              require('autoprefixer')()
-            ]
-          } },
-          'sass-loader'
+          {
+            loader: 'postcss-loader',
+            options: {
+              sourceMap: false,
+              plugins: (loader) => {
+                return [
+                  require('autoprefixer')(),
+                ];
+              },
+            },
+          },
+          'sass-loader',
         ],
-        exclude: [helpers.root('src/client/js/legacy')]
+        exclude: [helpers.root('src/client/js/legacy')],
       },
       {
         test: /\.(css|scss)$/,
         use: ['style-loader', 'css-loader', 'sass-loader'],
-        include: [helpers.root('src/client/js/legacy')]
+        include: [helpers.root('src/client/js/legacy')],
       },
-    ]
+    ],
   },
   plugins: [
 
@@ -63,7 +69,7 @@ module.exports = require('./webpack.common')({
   optimization: {
     minimizer: [
       new TerserPlugin({}),
-      new OptimizeCSSAssetsPlugin({})
+      new OptimizeCSSAssetsPlugin({}),
     ],
   },
 });

+ 13 - 3
package.json

@@ -32,8 +32,11 @@
     "clean:report": "rimraf -- report",
     "clean": "npm-run-all -p clean:*",
     "heroku-postbuild": "sh bin/heroku/install-plugins.sh && npm run build:prod",
-    "lint:fix": "eslint . --fix",
-    "lint": "eslint .",
+    "lint:js:fix": "eslint . --fix",
+    "lint:js": "eslint .",
+    "lint:styles:fix": "prettier-stylelint --quiet --write src/client/styles/scss/**/*.scss",
+    "lint:styles": "stylelint src/client/styles/scss/**/*.scss",
+    "lint": "npm-run-all -p lint:js lint:styles",
     "migrate": "npm run migrate:up",
     "migrate:create": "migrate-mongo create -f config/migrate.js -- ",
     "migrate:status": "migrate-mongo status -f config/migrate.js",
@@ -127,6 +130,7 @@
     "@handsontable/react": "^2.0.0",
     "autoprefixer": "^9.0.0",
     "babel-core": "^6.25.0",
+    "babel-eslint": "^10.0.1",
     "babel-loader": "^7.1.1",
     "babel-plugin-lodash": "^3.3.2",
     "babel-plugin-transform-runtime": "^6.23.0",
@@ -150,7 +154,11 @@
     "diff2html": "^2.3.3",
     "eazy-logger": "^3.0.2",
     "eslint": "^5.0.0",
-    "eslint-plugin-react": "^7.7.0",
+    "eslint-config-airbnb": "^17.1.0",
+    "eslint-plugin-chai-friendly": "^0.4.1",
+    "eslint-plugin-import": "^2.16.0",
+    "eslint-plugin-jsx-a11y": "^6.2.1",
+    "eslint-plugin-react": "^7.12.4",
     "file-loader": "^3.0.1",
     "handsontable": "^6.0.1",
     "i18next-browser-languagedetector": "^3.0.1",
@@ -184,6 +192,7 @@
     "penpal": "^3.0.3",
     "plantuml-encoder": "^1.2.5",
     "postcss-loader": "^3.0.0",
+    "prettier-stylelint": "^0.4.2",
     "react": "^16.8.3",
     "react-bootstrap": "^0.32.1",
     "react-bootstrap-typeahead": "^3.3.4",
@@ -202,6 +211,7 @@
     "sinon-chai": "^3.3.0",
     "socket.io-client": "^2.0.3",
     "style-loader": "^0.23.0",
+    "stylelint-config-recess-order": "^2.0.1",
     "terser-webpack-plugin": "^1.2.2",
     "throttle-debounce": "^2.0.0",
     "toastr": "^2.1.2",

+ 36 - 36
resource/cdn-manifests.js

@@ -6,7 +6,7 @@ module.exports = {
       groups: ['basis'],
       args: {
         integrity: '',
-      }
+      },
     },
     {
       name: 'highlight',
@@ -14,25 +14,25 @@ module.exports = {
       groups: ['basis'],
       args: {
         integrity: '',
-      }
+      },
     },
     {
       name: 'highlight-addons',
-      url: 'https://cdn.jsdelivr.net/combine/' +
-'gh/highlightjs/cdn-release@9.13.0/build/languages/dockerfile.min.js,' +
-'gh/highlightjs/cdn-release@9.13.0/build/languages/go.min.js,' +
-'gh/highlightjs/cdn-release@9.13.0/build/languages/gradle.min.js,' +
-'gh/highlightjs/cdn-release@9.13.0/build/languages/json.min.js,' +
-'gh/highlightjs/cdn-release@9.13.0/build/languages/less.min.js,' +
-'gh/highlightjs/cdn-release@9.13.0/build/languages/plaintext.min.js,' +
-'gh/highlightjs/cdn-release@9.13.0/build/languages/scss.min.js,' +
-'gh/highlightjs/cdn-release@9.13.0/build/languages/typescript.min.js,' +
-'gh/highlightjs/cdn-release@9.13.0/build/languages/yaml.min.js,' +
-'npm/highlightjs-line-numbers.js@2.6.0/dist/highlightjs-line-numbers.min.js',
+      url: 'https://cdn.jsdelivr.net/combine/'
+        + 'gh/highlightjs/cdn-release@9.13.0/build/languages/dockerfile.min.js,'
+        + 'gh/highlightjs/cdn-release@9.13.0/build/languages/go.min.js,'
+        + 'gh/highlightjs/cdn-release@9.13.0/build/languages/gradle.min.js,'
+        + 'gh/highlightjs/cdn-release@9.13.0/build/languages/json.min.js,'
+        + 'gh/highlightjs/cdn-release@9.13.0/build/languages/less.min.js,'
+        + 'gh/highlightjs/cdn-release@9.13.0/build/languages/plaintext.min.js,'
+        + 'gh/highlightjs/cdn-release@9.13.0/build/languages/scss.min.js,'
+        + 'gh/highlightjs/cdn-release@9.13.0/build/languages/typescript.min.js,'
+        + 'gh/highlightjs/cdn-release@9.13.0/build/languages/yaml.min.js,'
+        + 'npm/highlightjs-line-numbers.js@2.6.0/dist/highlightjs-line-numbers.min.js',
       args: {
         async: true,
         integrity: '',
-      }
+      },
     },
     {
       name: 'mathjax',
@@ -40,35 +40,35 @@ module.exports = {
       args: {
         async: true,
         integrity: '',
-      }
+      },
     },
     {
       name: 'codemirror-dialog',
       url: 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0/addon/dialog/dialog.min.js',
       args: {
         integrity: '',
-      }
+      },
     },
     {
       name: 'codemirror-keymap-vim',
       url: 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0/keymap/vim.min.js',
       args: {
         integrity: '',
-      }
+      },
     },
     {
       name: 'codemirror-keymap-emacs',
       url: 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0/keymap/emacs.min.js',
       args: {
         integrity: '',
-      }
+      },
     },
     {
       name: 'codemirror-keymap-sublime',
       url: 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0/keymap/sublime.min.js',
       args: {
         integrity: '',
-      }
+      },
     },
   ],
   style: [
@@ -77,7 +77,7 @@ module.exports = {
       url: 'https://fonts.googleapis.com/css?family=Lato:400,700',
       groups: ['basis'],
       args: {
-        integrity: ''
+        integrity: '',
       },
     },
     {
@@ -86,14 +86,14 @@ module.exports = {
       groups: ['basis'],
       args: {
         integrity: '',
-      }
+      },
     },
     {
       name: 'themify-icons',
       url: 'https://cdn.jsdelivr.net/npm/cd-themify-icons@0.0.1/index.min.css',
       groups: ['basis'],
       args: {
-        integrity: ''
+        integrity: '',
       },
     },
     {
@@ -101,7 +101,7 @@ module.exports = {
       url: 'https://cdn.jsdelivr.net/npm/simple-line-icons@2.4.1/css/simple-line-icons.min.css',
       groups: ['basis'],
       args: {
-        integrity: ''
+        integrity: '',
       },
     },
     {
@@ -109,21 +109,21 @@ module.exports = {
       url: 'https://cdn.jsdelivr.net/npm/emojione@3.1.2/extras/css/emojione.min.css',
       groups: ['basis'],
       args: {
-        integrity: ''
+        integrity: '',
       },
     },
     {
       name: 'jquery-ui',
       url: 'https://cdn.jsdelivr.net/jquery.ui/1.11.4/jquery-ui.min.css',
       args: {
-        integrity: ''
+        integrity: '',
       },
     },
     {
       name: 'highlight-theme-github',
       url: 'https://cdn.jsdelivr.net/npm/highlight.js@9.13.0/styles/github.css',
       args: {
-        integrity: ''
+        integrity: '',
       },
     },
     {
@@ -131,63 +131,63 @@ module.exports = {
       url: 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0/addon/dialog/dialog.min.css',
       args: {
         integrity: '',
-      }
+      },
     },
     {
       name: 'codemirror-theme-eclipse',
       url: 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0/theme/eclipse.min.css',
       args: {
-        integrity: ''
+        integrity: '',
       },
     },
     {
       name: 'codemirror-theme-elegant',
       url: 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0/theme/elegant.min.css',
       args: {
-        integrity: ''
+        integrity: '',
       },
     },
     {
       name: 'codemirror-theme-neo',
       url: 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0/theme/neo.min.css',
       args: {
-        integrity: ''
+        integrity: '',
       },
     },
     {
       name: 'codemirror-theme-mdn-like',
       url: 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0/theme/mdn-like.min.css',
       args: {
-        integrity: ''
+        integrity: '',
       },
     },
     {
       name: 'codemirror-theme-material',
       url: 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0/theme/material.min.css',
       args: {
-        integrity: ''
+        integrity: '',
       },
     },
     {
       name: 'codemirror-theme-dracula',
       url: 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0/theme/dracula.min.css',
       args: {
-        integrity: ''
+        integrity: '',
       },
     },
     {
       name: 'codemirror-theme-monokai',
       url: 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0/theme/monokai.min.css',
       args: {
-        integrity: ''
+        integrity: '',
       },
     },
     {
       name: 'codemirror-theme-twilight',
       url: 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0/theme/twilight.min.css',
       args: {
-        integrity: ''
+        integrity: '',
       },
     },
-  ]
+  ],
 };

+ 125 - 87
src/client/js/app.js

@@ -1,45 +1,50 @@
+/* eslint-disable max-len */
+
 import React from 'react';
 import ReactDOM from 'react-dom';
 import { I18nextProvider } from 'react-i18next';
 import * as toastr from 'toastr';
 
-import i18nFactory from './i18n';
-
 import loggerFactory from '@alias/logger';
 import Xss from '@commons/service/xss';
+import * as entities from 'entities';
+import i18nFactory from './i18n';
+
 
 import Crowi from './util/Crowi';
 // import CrowiRenderer from './util/CrowiRenderer';
 import GrowiRenderer from './util/GrowiRenderer';
 
-import HeaderSearchBox  from './components/HeaderSearchBox';
-import SearchPage       from './components/SearchPage';
-import PageEditor       from './components/PageEditor';
-import OptionsSelector  from './components/PageEditor/OptionsSelector';
+import HeaderSearchBox from './components/HeaderSearchBox';
+import SearchPage from './components/SearchPage';
+import PageEditor from './components/PageEditor';
+// eslint-disable-next-line import/no-duplicates
+import OptionsSelector from './components/PageEditor/OptionsSelector';
+// eslint-disable-next-line import/no-duplicates
 import { EditorOptions, PreviewOptions } from './components/PageEditor/OptionsSelector';
 import SavePageControls from './components/SavePageControls';
 import PageEditorByHackmd from './components/PageEditorByHackmd';
-import Page             from './components/Page';
-import PageHistory      from './components/PageHistory';
-import PageComments     from './components/PageComments';
+import Page from './components/Page';
+import PageHistory from './components/PageHistory';
+import PageComments from './components/PageComments';
 import CommentForm from './components/PageComment/CommentForm';
-import PageAttachment   from './components/PageAttachment';
-import PageStatusAlert  from './components/PageStatusAlert';
-import RevisionPath     from './components/Page/RevisionPath';
-import PageTagForm      from './components/PageTagForm';
-import RevisionUrl      from './components/Page/RevisionUrl';
-import BookmarkButton   from './components/BookmarkButton';
-import LikeButton       from './components/LikeButton';
+import PageAttachment from './components/PageAttachment';
+import PageStatusAlert from './components/PageStatusAlert';
+import RevisionPath from './components/Page/RevisionPath';
+// TODO GC-1430 activate
+// import PageTagForm from './components/PageTagForm';
+import RevisionUrl from './components/Page/RevisionUrl';
+import BookmarkButton from './components/BookmarkButton';
+import LikeButton from './components/LikeButton';
 import PagePathAutoComplete from './components/PagePathAutoComplete';
 import RecentCreated from './components/RecentCreated/RecentCreated';
-import UserPictureList  from './components/Common/UserPictureList';
+import UserPictureList from './components/Common/UserPictureList';
 
-import CustomCssEditor  from './components/Admin/CustomCssEditor';
+import CustomCssEditor from './components/Admin/CustomCssEditor';
 import CustomScriptEditor from './components/Admin/CustomScriptEditor';
 import CustomHeaderEditor from './components/Admin/CustomHeaderEditor';
 import AdminRebuildSearch from './components/Admin/AdminRebuildSearch';
 
-import * as entities from 'entities';
 
 const logger = loggerFactory('growi:app');
 
@@ -65,8 +70,9 @@ let pagePath;
 let pageContent = '';
 let markdown = '';
 let slackChannels;
-let currentPageTags = '';
-let newPageTags = '';
+// TODO GC-1430 activate
+// const currentPageTags = '';
+// let newPageTags = '';
 if (mainContent !== null) {
   pageId = mainContent.getAttribute('data-page-id') || null;
   pageRevisionId = mainContent.getAttribute('data-page-revision-id');
@@ -100,8 +106,8 @@ const socketClientId = crowi.getSocketClientId();
 
 const crowiRenderer = new GrowiRenderer(crowi, null, {
   mode: 'page',
-  isAutoSetup: false,                                     // manually setup because plugins may configure it
-  renderToc: crowi.getCrowiForJquery().renderTocContent,  // function for rendering Table Of Contents
+  isAutoSetup: false, // manually setup because plugins may configure it
+  renderToc: crowi.getCrowiForJquery().renderTocContent, // function for rendering Table Of Contents
 });
 window.crowiRenderer = crowiRenderer;
 
@@ -116,14 +122,15 @@ if (isEnabledPlugins) {
  * get new tags from page tag form
  * @param {String} tags new tags [TODO] String -> Array
  */
-const getNewPageTags = function(tags) {
-  newPageTags = tags;
-};
+// TODO GC-1430 activate
+// const getNewPageTags = function(tags) {
+//   newPageTags = tags;
+// };
 
 /**
  * component store
  */
-let componentInstances = {};
+const componentInstances = {};
 
 /**
  * save success handler when reloading is not needed
@@ -198,7 +205,8 @@ const saveWithShortcut = function(markdown) {
   // get options
   const options = componentInstances.savePageControls.getCurrentOptionsToSave();
   options.socketClientId = socketClientId;
-  options.pageTags = newPageTags;
+  // TODO GC-1430 activate
+  // options.pageTags = newPageTags;
 
   if (editorMode === 'hackmd') {
     // set option to sync
@@ -207,7 +215,7 @@ const saveWithShortcut = function(markdown) {
     revisionId = componentInstances.pageEditorByHackmd.getRevisionIdHackmdSynced();
   }
 
-  let promise = undefined;
+  let promise;
   if (pageId == null) {
     promise = crowi.createPage(pagePath, markdown, options);
   }
@@ -222,7 +230,7 @@ const saveWithShortcut = function(markdown) {
 
 const saveWithSubmitButtonSuccessHandler = function() {
   crowi.clearDraft(pagePath);
-  location.href = pagePath;
+  window.location.href = pagePath;
 };
 
 const saveWithSubmitButton = function(submitOpts) {
@@ -236,12 +244,13 @@ const saveWithSubmitButton = function(submitOpts) {
   // get options
   const options = componentInstances.savePageControls.getCurrentOptionsToSave();
   options.socketClientId = socketClientId;
-  options.pageTags = newPageTags;
+  // TODO GC-1430 activate
+  // options.pageTags = newPageTags;
 
   // set 'submitOpts.overwriteScopesOfDescendants' to options
   options.overwriteScopesOfDescendants = submitOpts ? !!submitOpts.overwriteScopesOfDescendants : false;
 
-  let promise = undefined;
+  let promise;
   if (editorMode === 'hackmd') {
     // get markdown
     promise = componentInstances.pageEditorByHackmd.getMarkdown();
@@ -256,12 +265,12 @@ const saveWithSubmitButton = function(submitOpts) {
   }
   // create or update
   if (pageId == null) {
-    promise = promise.then(markdown => {
+    promise = promise.then((markdown) => {
       return crowi.createPage(pagePath, markdown, options);
     });
   }
   else {
-    promise = promise.then(markdown => {
+    promise = promise.then((markdown) => {
       return crowi.updatePage(pageId, revisionId, markdown, options);
     });
   }
@@ -290,11 +299,11 @@ const componentMappings = {
   'search-sidebar': <HeaderSearchBox crowi={crowi} />,
   'search-page': <I18nextProvider i18n={i18n}><SearchPage crowi={crowi} crowiRenderer={crowiRenderer} /></I18nextProvider>,
 
-  //'revision-history': <PageHistory pageId={pageId} />,
+  // 'revision-history': <PageHistory pageId={pageId} />,
   'bookmark-button': <BookmarkButton pageId={pageId} crowi={crowi} />,
   'bookmark-button-lg': <BookmarkButton pageId={pageId} crowi={crowi} size="lg" />,
 
-  'create-page-name-input': <PagePathAutoComplete crowi={crowi} initializedPath={pagePath} addTrailingSlash={true} />,
+  'create-page-name-input': <PagePathAutoComplete crowi={crowi} initializedPath={pagePath} addTrailingSlash />,
   'rename-page-name-input': <PagePathAutoComplete crowi={crowi} initializedPath={pagePath} />,
   'duplicate-page-name-input': <PagePathAutoComplete crowi={crowi} initializedPath={pagePath} />,
 
@@ -305,7 +314,7 @@ if (pageId) {
   componentMappings['page-attachment'] = <PageAttachment pageId={pageId} markdown={markdown} crowi={crowi} />;
 }
 if (pagePath) {
-  componentMappings['page'] = <Page crowi={crowi} crowiRenderer={crowiRenderer} markdown={markdown} pagePath={pagePath} showHeadEditButton={true} onSaveWithShortcut={saveWithShortcut} />;
+  componentMappings.page = <Page crowi={crowi} crowiRenderer={crowiRenderer} markdown={markdown} pagePath={pagePath} showHeadEditButton onSaveWithShortcut={saveWithShortcut} />;
   componentMappings['revision-path'] = <RevisionPath pagePath={pagePath} crowi={crowi} />;
   // componentMappings['page-tag'] = <PageTagForm pageTags={currentPageTags} submitTags={getNewPageTags} />; [pagetag]
   componentMappings['revision-url'] = <RevisionUrl pageId={pageId} pagePath={pagePath} />;
@@ -319,8 +328,8 @@ Object.keys(componentMappings).forEach((key) => {
 });
 
 // set page if exists
-if (componentInstances['page'] != null) {
-  crowi.setPage(componentInstances['page']);
+if (componentInstances.page != null) {
+  crowi.setPage(componentInstances.page);
 }
 
 // render LikeButton
@@ -329,7 +338,7 @@ if (likeButtonElem) {
   const isLiked = likeButtonElem.dataset.liked === 'true';
   ReactDOM.render(
     <LikeButton crowi={crowi} pageId={pageId} isLiked={isLiked} />,
-    likeButtonElem
+    likeButtonElem,
   );
 }
 
@@ -340,7 +349,7 @@ if (seenUserListElem) {
   const userIds = userIdsStr.split(',');
   ReactDOM.render(
     <UserPictureList crowi={crowi} userIds={userIds} />,
-    seenUserListElem
+    seenUserListElem,
   );
 }
 // render UserPictureList for liker-list
@@ -350,7 +359,7 @@ if (likerListElem) {
   const userIds = userIdsStr.split(',');
   ReactDOM.render(
     <UserPictureList crowi={crowi} userIds={userIds} />,
-    likerListElem
+    likerListElem,
   );
 }
 
@@ -363,16 +372,23 @@ if (savePageControlsElem) {
   const grantGroupName = savePageControlsElem.dataset.grantGroupName;
   ReactDOM.render(
     <I18nextProvider i18n={i18n}>
-      <SavePageControls crowi={crowi} onSubmit={saveWithSubmitButton}
-          ref={(elem) => {
+      <SavePageControls
+        crowi={crowi}
+        onSubmit={saveWithSubmitButton}
+        ref={(elem) => {
             if (savePageControls == null) {
               savePageControls = elem.getWrappedInstance();
             }
           }}
-          pageId={pageId} pagePath={pagePath} slackChannels={slackChannels}
-          grant={grant} grantGroupId={grantGroupId} grantGroupName={grantGroupName} />
+        pageId={pageId}
+        pagePath={pagePath}
+        slackChannels={slackChannels}
+        grant={grant}
+        grantGroupId={grantGroupId}
+        grantGroupName={grantGroupName}
+      />
     </I18nextProvider>,
-    savePageControlsElem
+    savePageControlsElem,
   );
   componentInstances.savePageControls = savePageControls;
 }
@@ -380,13 +396,13 @@ if (savePageControlsElem) {
 const recentCreatedControlsElem = document.getElementById('user-created-list');
 if (recentCreatedControlsElem) {
   let limit = crowi.getConfig().recentCreatedLimit;
-  if (null == limit) {
+  if (limit == null) {
     limit = 10;
   }
   ReactDOM.render(
-    <RecentCreated  crowi={crowi} pageId={pageId} limit={limit} >
+    <RecentCreated crowi={crowi} pageId={pageId} limit={limit}>
 
-    </RecentCreated>, document.getElementById('user-created-list')
+    </RecentCreated>, document.getElementById('user-created-list'),
   );
 }
 
@@ -398,12 +414,17 @@ let pageEditorByHackmd = null;
 const pageEditorWithHackmdElem = document.getElementById('page-editor-with-hackmd');
 if (pageEditorWithHackmdElem) {
   pageEditorByHackmd = ReactDOM.render(
-    <PageEditorByHackmd crowi={crowi}
-        pageId={pageId} revisionId={pageRevisionId}
-        pageIdOnHackmd={pageIdOnHackmd} revisionIdHackmdSynced={pageRevisionIdHackmdSynced} hasDraftOnHackmd={hasDraftOnHackmd}
-        markdown={markdown}
-        onSaveWithShortcut={saveWithShortcut} />,
-    pageEditorWithHackmdElem
+    <PageEditorByHackmd
+      crowi={crowi}
+      pageId={pageId}
+      revisionId={pageRevisionId}
+      pageIdOnHackmd={pageIdOnHackmd}
+      revisionIdHackmdSynced={pageRevisionIdHackmdSynced}
+      hasDraftOnHackmd={hasDraftOnHackmd}
+      markdown={markdown}
+      onSaveWithShortcut={saveWithShortcut}
+    />,
+    pageEditorWithHackmdElem,
   );
   componentInstances.pageEditorByHackmd = pageEditorByHackmd;
 }
@@ -419,12 +440,18 @@ const previewOptions = new PreviewOptions(crowi.previewOptions);
 const pageEditorElem = document.getElementById('page-editor');
 if (pageEditorElem) {
   pageEditor = ReactDOM.render(
-    <PageEditor crowi={crowi} crowiRenderer={crowiRenderer}
-        pageId={pageId} revisionId={pageRevisionId} pagePath={pagePath}
-        markdown={markdown}
-        editorOptions={editorOptions} previewOptions={previewOptions}
-        onSaveWithShortcut={saveWithShortcut} />,
-    pageEditorElem
+    <PageEditor
+      crowi={crowi}
+      crowiRenderer={crowiRenderer}
+      pageId={pageId}
+      revisionId={pageRevisionId}
+      pagePath={pagePath}
+      markdown={markdown}
+      editorOptions={editorOptions}
+      previewOptions={previewOptions}
+      onSaveWithShortcut={saveWithShortcut}
+    />,
+    pageEditorElem,
   );
   componentInstances.pageEditor = pageEditor;
   // set refs for setCaretLine/forceToFocus when tab is changed
@@ -441,15 +468,18 @@ if (writeCommentElem) {
     }
   };
   ReactDOM.render(
-    <CommentForm crowi={crowi}
+    <CommentForm
+      crowi={crowi}
       crowiOriginRenderer={crowiRenderer}
       pageId={pageId}
       pagePath={pagePath}
       revisionId={pageRevisionId}
       onPostComplete={postCompleteHandler}
       editorOptions={editorOptions}
-      slackChannels = {slackChannels}/>,
-    writeCommentElem);
+      slackChannels={slackChannels}
+    />,
+    writeCommentElem,
+  );
 }
 
 // render OptionsSelector
@@ -457,8 +487,10 @@ const pageEditorOptionsSelectorElem = document.getElementById('page-editor-optio
 if (pageEditorOptionsSelectorElem) {
   ReactDOM.render(
     <I18nextProvider i18n={i18n}>
-      <OptionsSelector crowi={crowi}
-        editorOptions={editorOptions} previewOptions={previewOptions}
+      <OptionsSelector
+        crowi={crowi}
+        editorOptions={editorOptions}
+        previewOptions={previewOptions}
         onChange={(newEditorOptions, newPreviewOptions) => { // set onChange event handler
           // set options
           pageEditor.setEditorOptions(newEditorOptions);
@@ -466,9 +498,10 @@ if (pageEditorOptionsSelectorElem) {
           // save
           crowi.saveEditorOptions(newEditorOptions);
           crowi.savePreviewOptions(newPreviewOptions);
-        }} />
+        }}
+      />
     </I18nextProvider>,
-    pageEditorOptionsSelectorElem
+    pageEditorOptionsSelectorElem,
   );
 }
 
@@ -478,15 +511,19 @@ const pageStatusAlertElem = document.getElementById('page-status-alert');
 if (pageStatusAlertElem) {
   ReactDOM.render(
     <I18nextProvider i18n={i18n}>
-      <PageStatusAlert crowi={crowi}
-          ref={(elem) => {
+      <PageStatusAlert
+        crowi={crowi}
+        ref={(elem) => {
             if (pageStatusAlert == null) {
               pageStatusAlert = elem.getWrappedInstance();
             }
           }}
-          revisionId={pageRevisionId} revisionIdHackmdSynced={pageRevisionIdHackmdSynced} hasDraftOnHackmd={hasDraftOnHackmd} />
+        revisionId={pageRevisionId}
+        revisionIdHackmdSynced={pageRevisionIdHackmdSynced}
+        hasDraftOnHackmd={hasDraftOnHackmd}
+      />
     </I18nextProvider>,
-    pageStatusAlertElem
+    pageStatusAlertElem,
   );
   componentInstances.pageStatusAlert = pageStatusAlert;
 }
@@ -499,7 +536,7 @@ if (customCssEditorElem != null) {
 
   ReactDOM.render(
     <CustomCssEditor inputElem={customCssInputElem} />,
-    customCssEditorElem
+    customCssEditorElem,
   );
 }
 const customScriptEditorElem = document.getElementById('custom-script-editor');
@@ -509,7 +546,7 @@ if (customScriptEditorElem != null) {
 
   ReactDOM.render(
     <CustomScriptEditor inputElem={customScriptInputElem} />,
-    customScriptEditorElem
+    customScriptEditorElem,
   );
 }
 const customHeaderEditorElem = document.getElementById('custom-header-editor');
@@ -519,14 +556,14 @@ if (customHeaderEditorElem != null) {
 
   ReactDOM.render(
     <CustomHeaderEditor inputElem={customHeaderInputElem} />,
-    customHeaderEditorElem
+    customHeaderEditorElem,
   );
 }
 const adminRebuildSearchElem = document.getElementById('admin-rebuild-search');
 if (adminRebuildSearchElem != null) {
   ReactDOM.render(
     <AdminRebuildSearch crowi={crowi} />,
-    adminRebuildSearchElem
+    adminRebuildSearchElem,
   );
 }
 
@@ -540,7 +577,7 @@ function updatePageStatusAlert(page, user) {
     pageStatusAlert.setLastUpdateUsername(user.name);
   }
 }
-socket.on('page:create', function(data) {
+socket.on('page:create', (data) => {
   // skip if triggered myself
   if (data.socketClientId != null && data.socketClientId === socketClientId) {
     return;
@@ -549,11 +586,11 @@ socket.on('page:create', function(data) {
   logger.debug({ obj: data }, `websocket on 'page:create'`); // eslint-disable-line quotes
 
   // update PageStatusAlert
-  if (data.page.path == pagePath) {
+  if (data.page.path === pagePath) {
     updatePageStatusAlert(data.page, data.user);
   }
 });
-socket.on('page:update', function(data) {
+socket.on('page:update', (data) => {
   // skip if triggered myself
   if (data.socketClientId != null && data.socketClientId === socketClientId) {
     return;
@@ -561,7 +598,7 @@ socket.on('page:update', function(data) {
 
   logger.debug({ obj: data }, `websocket on 'page:update'`); // eslint-disable-line quotes
 
-  if (data.page.path == pagePath) {
+  if (data.page.path === pagePath) {
     // update PageStatusAlert
     updatePageStatusAlert(data.page, data.user);
     // update PageEditorByHackmd
@@ -573,7 +610,7 @@ socket.on('page:update', function(data) {
     }
   }
 });
-socket.on('page:delete', function(data) {
+socket.on('page:delete', (data) => {
   // skip if triggered myself
   if (data.socketClientId != null && data.socketClientId === socketClientId) {
     return;
@@ -582,11 +619,11 @@ socket.on('page:delete', function(data) {
   logger.debug({ obj: data }, `websocket on 'page:delete'`); // eslint-disable-line quotes
 
   // update PageStatusAlert
-  if (data.page.path == pagePath) {
+  if (data.page.path === pagePath) {
     updatePageStatusAlert(data.page, data.user);
   }
 });
-socket.on('page:editingWithHackmd', function(data) {
+socket.on('page:editingWithHackmd', (data) => {
   // skip if triggered myself
   if (data.socketClientId != null && data.socketClientId === socketClientId) {
     return;
@@ -594,7 +631,7 @@ socket.on('page:editingWithHackmd', function(data) {
 
   logger.debug({ obj: data }, `websocket on 'page:editingWithHackmd'`); // eslint-disable-line quotes
 
-  if (data.page.path == pagePath) {
+  if (data.page.path === pagePath) {
     // update PageStatusAlert
     const pageStatusAlert = componentInstances.pageStatusAlert;
     if (pageStatusAlert != null) {
@@ -609,9 +646,10 @@ socket.on('page:editingWithHackmd', function(data) {
 });
 
 // うわーもうー (commented by Crowi team -- 2018.03.23 Yuki Takei)
-$('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', function() {
+$('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', () => {
   ReactDOM.render(
     <I18nextProvider i18n={i18n}>
       <PageHistory pageId={pageId} crowi={crowi} />
-    </I18nextProvider>, document.getElementById('revision-history'));
+    </I18nextProvider>, document.getElementById('revision-history'),
+  );
 });

+ 6 - 9
src/client/js/components/Admin/CustomCssEditor.js

@@ -2,6 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 
 import { UnControlled as CodeMirror } from 'react-codemirror2';
+
 require('codemirror/addon/lint/css-lint');
 require('codemirror/addon/hint/css-hint');
 require('codemirror/addon/hint/show-hint');
@@ -14,10 +15,6 @@ require('jquery-ui/ui/widgets/resizable');
 
 export default class CustomCssEditor extends React.Component {
 
-  constructor(props) {
-    super(props);
-  }
-
   render() {
     // get initial value from inputElem
     const value = this.props.inputElem.value;
@@ -25,24 +22,24 @@ export default class CustomCssEditor extends React.Component {
     return (
       <CodeMirror
         value={value}
-        autoFocus={true}
+        autoFocus
         options={{
           mode: 'css',
           lineNumbers: true,
           tabSize: 2,
           indentUnit: 2,
           theme: 'eclipse',
-          autoRefresh: {force: true},   // force option is enabled by autorefresh.ext.js -- Yuki Takei
+          autoRefresh: { force: true }, // force option is enabled by autorefresh.ext.js -- Yuki Takei
           matchBrackets: true,
           autoCloseBrackets: true,
-          extraKeys: {'Ctrl-Space': 'autocomplete'},
+          extraKeys: { 'Ctrl-Space': 'autocomplete' },
         }}
         editorDidMount={(editor, next) => {
           // resizable with jquery.ui
           $(editor.getWrapperElement()).resizable({
-            resize: function() {
+            resize() {
               editor.setSize($(this).width(), $(this).height());
-            }
+            },
           });
         }}
         onChange={(editor, data, value) => {

+ 6 - 9
src/client/js/components/Admin/CustomHeaderEditor.js

@@ -2,6 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 
 import { UnControlled as CodeMirror } from 'react-codemirror2';
+
 require('codemirror/addon/hint/show-hint');
 require('codemirror/addon/edit/matchbrackets');
 require('codemirror/addon/edit/closebrackets');
@@ -12,10 +13,6 @@ require('jquery-ui/ui/widgets/resizable');
 
 export default class CustomHeaderEditor extends React.Component {
 
-  constructor(props) {
-    super(props);
-  }
-
   render() {
     // get initial value from inputElem
     const value = this.props.inputElem.value;
@@ -23,24 +20,24 @@ export default class CustomHeaderEditor extends React.Component {
     return (
       <CodeMirror
         value={value}
-        autoFocus={true}
+        autoFocus
         options={{
           mode: 'htmlmixed',
           lineNumbers: true,
           tabSize: 2,
           indentUnit: 2,
           theme: 'eclipse',
-          autoRefresh: {force: true},   // force option is enabled by autorefresh.ext.js -- Yuki Takei
+          autoRefresh: { force: true }, // force option is enabled by autorefresh.ext.js -- Yuki Takei
           matchBrackets: true,
           autoCloseBrackets: true,
-          extraKeys: {'Ctrl-Space': 'autocomplete'},
+          extraKeys: { 'Ctrl-Space': 'autocomplete' },
         }}
         editorDidMount={(editor, next) => {
           // resizable with jquery.ui
           $(editor.getWrapperElement()).resizable({
-            resize: function() {
+            resize() {
               editor.setSize($(this).width(), $(this).height());
-            }
+            },
           });
         }}
         onChange={(editor, data, value) => {

+ 6 - 9
src/client/js/components/Admin/CustomScriptEditor.js

@@ -2,6 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 
 import { UnControlled as CodeMirror } from 'react-codemirror2';
+
 require('codemirror/addon/lint/javascript-lint');
 require('codemirror/addon/hint/javascript-hint');
 require('codemirror/addon/hint/show-hint');
@@ -14,10 +15,6 @@ require('jquery-ui/ui/widgets/resizable');
 
 export default class CustomScriptEditor extends React.Component {
 
-  constructor(props) {
-    super(props);
-  }
-
   render() {
     // get initial value from inputElem
     const value = this.props.inputElem.value;
@@ -25,24 +22,24 @@ export default class CustomScriptEditor extends React.Component {
     return (
       <CodeMirror
         value={value}
-        autoFocus={true}
+        autoFocus
         options={{
           mode: 'javascript',
           lineNumbers: true,
           tabSize: 2,
           indentUnit: 2,
           theme: 'eclipse',
-          autoRefresh: {force: true},   // force option is enabled by autorefresh.ext.js -- Yuki Takei
+          autoRefresh: { force: true }, // force option is enabled by autorefresh.ext.js -- Yuki Takei
           matchBrackets: true,
           autoCloseBrackets: true,
-          extraKeys: {'Ctrl-Space': 'autocomplete'},
+          extraKeys: { 'Ctrl-Space': 'autocomplete' },
         }}
         editorDidMount={(editor, next) => {
           // resizable with jquery.ui
           $(editor.getWrapperElement()).resizable({
-            resize: function() {
+            resize() {
               editor.setSize($(this).width(), $(this).height());
-            }
+            },
           });
         }}
         onChange={(editor, data, value) => {

+ 1 - 2
src/client/js/components/Common/UserDate.js

@@ -19,6 +19,7 @@ export default class UserDate extends React.Component {
       </span>
     );
   }
+
 }
 
 UserDate.propTypes = {
@@ -28,8 +29,6 @@ UserDate.propTypes = {
 };
 
 UserDate.defaultProps = {
-  dateTime: 'now',
   format: 'YYYY/MM/DD HH:mm:ss',
   className: '',
 };
-

+ 15 - 7
src/client/js/components/CopyButton.js

@@ -23,7 +23,7 @@ export default class CopyButton extends React.Component {
 
   render() {
     const containerStyle = {
-      lineHeight: 0
+      lineHeight: 0,
     };
     const style = Object.assign({
       padding: '0 2px',
@@ -34,16 +34,25 @@ export default class CopyButton extends React.Component {
 
     return (
       <span className="btn-copy-container" style={containerStyle}>
-        <ClipboardButton className={this.props.buttonClassName}
-            button-id={this.props.buttonId} button-data-toggle="tooltip" button-data-container="body" button-title="copied!" button-data-placement="bottom" button-data-trigger="manual"
-            button-style={style}
-            data-clipboard-text={text} onSuccess={this.showToolTip}>
+        <ClipboardButton
+          className={this.props.buttonClassName}
+          button-id={this.props.buttonId}
+          button-data-toggle="tooltip"
+          button-data-container="body"
+          button-title="copied!"
+          button-data-placement="bottom"
+          button-data-trigger="manual"
+          button-style={style}
+          data-clipboard-text={text}
+          onSuccess={this.showToolTip}
+        >
 
-          <i className={this.props.iconClassName}></i>
+          <i className={this.props.iconClassName} />
         </ClipboardButton>
       </span>
     );
   }
+
 }
 
 CopyButton.propTypes = {
@@ -54,6 +63,5 @@ CopyButton.propTypes = {
   iconClassName: PropTypes.string.isRequired,
 };
 CopyButton.defaultProps = {
-  buttonId: 'btnCopy',
   buttonStyle: {},
 };

+ 4 - 3
src/client/js/components/Page/PagePath.js

@@ -10,14 +10,16 @@ export default class PagePath extends React.Component {
   render() {
     const page = this.props.page;
     const shortPath = this.getShortPath(page.path);
-    const pathPrefix = page.path.replace(new RegExp(shortPath + '(/)?$'), '');
+    const pathPrefix = page.path.replace(new RegExp(`${shortPath}(/)?$`), '');
 
     return (
       <span className="page-path">
-        {pathPrefix}<strong>{shortPath}</strong>
+        {pathPrefix}
+        <strong>{shortPath}</strong>
       </span>
     );
   }
+
 }
 
 PagePath.propTypes = {
@@ -25,5 +27,4 @@ PagePath.propTypes = {
 };
 
 PagePath.defaultProps = {
-  page: {},
 };

+ 6 - 4
src/client/js/components/Page/RevisionBody.js

@@ -39,7 +39,7 @@ export default class RevisionBody extends React.Component {
   }
 
   generateInnerHtml(html) {
-    return {__html: html};
+    return { __html: html };
   }
 
   render() {
@@ -52,15 +52,17 @@ export default class RevisionBody extends React.Component {
             this.props.inputRef(elm);
           }
         }}
-        className={`wiki ${additionalClassName}`} dangerouslySetInnerHTML={this.generateInnerHtml(this.props.html)}>
-      </div>
+        className={`wiki ${additionalClassName}`}
+        dangerouslySetInnerHTML={this.generateInnerHtml(this.props.html)}
+      />
     );
   }
+
 }
 
 RevisionBody.propTypes = {
   html: PropTypes.string,
-  inputRef: PropTypes.func,  // for getting div element
+  inputRef: PropTypes.func, // for getting div element
   isMathJaxEnabled: PropTypes.bool,
   renderMathJaxOnInit: PropTypes.bool,
   renderMathJaxInRealtime: PropTypes.bool,

+ 23 - 16
src/client/js/components/Page/RevisionPath.js

@@ -24,25 +24,25 @@ export default class RevisionPath extends React.Component {
     this.setState({ isListPage });
 
     // whether set link to '/'
-    const behaviorType = this.props.crowi.getConfig()['behaviorType'];
-    const isLinkToListPage = (!behaviorType || 'crowi' === behaviorType);
+    const behaviorType = this.props.crowi.getConfig().behaviorType;
+    const isLinkToListPage = (!behaviorType || behaviorType === 'crowi');
     this.setState({ isLinkToListPage });
 
     // generate pages obj
-    let splitted = this.props.pagePath.split(/\//);
-    splitted.shift();   // omit first element with shift()
-    if (splitted[splitted.length-1] === '') {
-      splitted.pop();   // omit last element with unshift()
+    const splitted = this.props.pagePath.split(/\//);
+    splitted.shift(); // omit first element with shift()
+    if (splitted[splitted.length - 1] === '') {
+      splitted.pop(); // omit last element with unshift()
     }
 
-    let pages = [];
+    const pages = [];
     let parentPath = '/';
     splitted.forEach((pageName) => {
       pages.push({
         pagePath: parentPath + encodeURIComponent(pageName),
         pageName: this.xss.process(pageName),
       });
-      parentPath += pageName + '/';
+      parentPath += `${pageName}/`;
     });
 
     this.setState({ pages });
@@ -56,8 +56,9 @@ export default class RevisionPath extends React.Component {
   }
 
   generateLinkElementToListPage(pagePath, isLinkToListPage, isLastElement) {
+    /* eslint-disable no-else-return */
     if (isLinkToListPage) {
-      return <a href={pagePath+'/'} className={(isLastElement && !this.state.isListPage) ? 'last-path' : ''}>/</a>;
+      return <a href={`${pagePath}/`} className={(isLastElement && !this.state.isListPage) ? 'last-path' : ''}>/</a>;
     }
     else if (!isLastElement) {
       return <span>/</span>;
@@ -65,6 +66,7 @@ export default class RevisionPath extends React.Component {
     else {
       return <span></span>;
     }
+    /* eslint-enable no-else-return */
   }
 
   render() {
@@ -85,20 +87,20 @@ export default class RevisionPath extends React.Component {
 
     const afterElements = [];
     this.state.pages.forEach((page, index) => {
-      const isLastElement = (index == pageLength-1);
+      const isLastElement = (index === pageLength - 1);
 
       // add elements for page
       afterElements.push(
         <span key={page.pagePath} className="path-segment">
           <a href={page.pagePath}>{page.pageName}</a>
-        </span>
+        </span>,
       );
 
       // add elements for '/'
       afterElements.push(
-        <span key={page.pagePath+'/'} className="separator" style={separatorStyle}>
+        <span key={`${page.pagePath}/`} className="separator" style={separatorStyle}>
           {this.generateLinkElementToListPage(page.pagePath, this.state.isLinkToListPage, isLastElement)}
-        </span>
+        </span>,
       );
     });
 
@@ -108,14 +110,19 @@ export default class RevisionPath extends React.Component {
           <a href="/">/</a>
         </span>
         {afterElements}
-        <CopyButton buttonId="btnCopyRevisionPath" text={this.props.pagePath}
-            buttonClassName="btn btn-default btn-copy" iconClassName="ti-clipboard" />
+        <CopyButton
+          buttonId="btnCopyRevisionPath"
+          text={this.props.pagePath}
+          buttonClassName="btn btn-default btn-copy"
+          iconClassName="ti-clipboard"
+        />
         <a href="#edit" className="btn btn-default btn-edit" style={editButtonStyle}>
-          <i className="icon-note"></i>
+          <i className="icon-note" />
         </a>
       </span>
     );
   }
+
 }
 
 RevisionPath.propTypes = {

+ 12 - 6
src/client/js/components/Page/RevisionUrl.js

@@ -14,24 +14,30 @@ export default class RevisionUrl extends React.Component {
 
   render() {
     const buttonStyle = {
-      fontSize: '1em'
+      fontSize: '1em',
     };
 
     const pagePath = this.xss.process(this.props.pagePath);
 
     const url = (this.props.pageId == null)
-      ? decodeURIComponent(location.href)
-      : `${location.origin}/${this.props.pageId}`;
-    const copiedText = pagePath + '\n' + url;
+      ? decodeURIComponent(window.location.href)
+      : `${window.location.origin}/${this.props.pageId}`;
+    const copiedText = `${pagePath}\n${url}`;
 
     return (
       <span>
         {url}
-        <CopyButton buttonId="btnCopyRevisionUrl" text={copiedText}
-            buttonClassName="btn btn-default btn-copy-link" buttonStyle={buttonStyle} iconClassName="ti-clipboard" />
+        <CopyButton
+          buttonId="btnCopyRevisionUrl"
+          text={copiedText}
+          buttonClassName="btn btn-default btn-copy-link"
+          buttonStyle={buttonStyle}
+          iconClassName="ti-clipboard"
+        />
       </span>
     );
   }
+
 }
 
 RevisionUrl.propTypes = {

+ 16 - 11
src/client/js/components/PageAttachment.js

@@ -1,3 +1,4 @@
+/* eslint-disable react/no-access-state-in-setstate */
 import React from 'react';
 import PropTypes from 'prop-types';
 
@@ -5,6 +6,7 @@ import PageAttachmentList from './PageAttachment/PageAttachmentList';
 import DeleteAttachmentModal from './PageAttachment/DeleteAttachmentModal';
 
 export default class PageAttachment extends React.Component {
+
   constructor(props) {
     super(props);
 
@@ -24,21 +26,21 @@ export default class PageAttachment extends React.Component {
     const pageId = this.props.pageId;
 
     if (!pageId) {
-      return ;
+      return;
     }
 
-    this.props.crowi.apiGet('/attachments.list', {page_id: pageId })
-      .then(res => {
+    this.props.crowi.apiGet('/attachments.list', { page_id: pageId })
+      .then((res) => {
         const attachments = res.attachments;
-        let inUse = {};
+        const inUse = {};
 
         for (const attachment of attachments) {
           inUse[attachment._id] = this.checkIfFileInUse(attachment);
         }
 
         this.setState({
-          attachments: attachments,
-          inUse: inUse,
+          attachments,
+          inUse,
         });
       });
   }
@@ -62,16 +64,18 @@ export default class PageAttachment extends React.Component {
       deleting: true,
     });
 
-    this.props.crowi.apiPost('/attachments.remove', {attachment_id: attachmentId})
-      .then(res => {
+    this.props.crowi.apiPost('/attachments.remove', { attachment_id: attachmentId })
+      .then((res) => {
         this.setState({
           attachments: this.state.attachments.filter((at) => {
+            // comparing ObjectId
+            // eslint-disable-next-line eqeqeq
             return at._id != attachmentId;
           }),
           attachmentToDelete: null,
           deleting: false,
         });
-      }).catch(err => {
+      }).catch((err) => {
         this.setState({
           deleteError: 'Something went wrong.',
           deleting: false,
@@ -87,10 +91,10 @@ export default class PageAttachment extends React.Component {
     let deleteAttachmentModal = '';
     if (this.isUserLoggedIn()) {
       const attachmentToDelete = this.state.attachmentToDelete;
-      let deleteModalClose = () => {
+      const deleteModalClose = () => {
         this.setState({ attachmentToDelete: null, deleteError: '' });
       };
-      let showModal = attachmentToDelete !== null;
+      const showModal = attachmentToDelete !== null;
 
       let deleteInUse = null;
       if (attachmentToDelete !== null) {
@@ -126,6 +130,7 @@ export default class PageAttachment extends React.Component {
       </div>
     );
   }
+
 }
 
 PageAttachment.propTypes = {

+ 10 - 5
src/client/js/components/PageAttachment/Attachment.js

@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
 import User from '../User/User';
 
 export default class Attachment extends React.Component {
+
   constructor(props) {
     super(props);
 
@@ -36,19 +37,22 @@ export default class Attachment extends React.Component {
     const btnDownload = (this.props.isUserLoggedIn)
       ? (
         <a className="attachment-download" href={attachment.downloadPathProxied}>
-          <i className="icon-cloud-download"></i>
-        </a>)
+          <i className="icon-cloud-download" />
+        </a>
+      )
       : '';
 
     const btnTrash = (this.props.isUserLoggedIn)
       ? (
+        /* eslint-disable-next-line */
         <a className="text-danger attachment-delete" onClick={this._onAttachmentDeleteClicked}>
-          <i className="icon-trash"></i>
-        </a>)
+          <i className="icon-trash" />
+        </a>
+      )
       : '';
 
     return (
-      <li className='attachment'>
+      <li className="attachment">
         <User user={attachment.creator} />
 
         <a href={attachment.filePathProxied}><i className={formatIcon}></i> {attachment.originalName}</a>
@@ -62,6 +66,7 @@ export default class Attachment extends React.Component {
       </li>
     );
   }
+
 }
 
 Attachment.propTypes = {

+ 11 - 5
src/client/js/components/PageAttachment/DeleteAttachmentModal.js

@@ -1,3 +1,4 @@
+/* eslint-disable react/prop-types */
 import React from 'react';
 import Button from 'react-bootstrap/es/Button';
 import Modal from 'react-bootstrap/es/Modal';
@@ -5,6 +6,7 @@ import Modal from 'react-bootstrap/es/Modal';
 import User from '../User/User';
 
 export default class DeleteAttachmentModal extends React.Component {
+
   constructor(props) {
     super(props);
 
@@ -25,7 +27,7 @@ export default class DeleteAttachmentModal extends React.Component {
 
   renderByFileFormat(attachment) {
     const content = (attachment.fileFormat.match(/image\/.+/i))
-      ? <img src={attachment.filePathProxied} />
+      ? <img src={attachment.filePathProxied} alt="deleting image" />
       : '';
 
 
@@ -63,7 +65,7 @@ export default class DeleteAttachmentModal extends React.Component {
       deletingIndicator = <span>{this.props.deleteError}</span>;
     }
 
-    let renderAttachment = this.renderByFileFormat(attachment);
+    const renderAttachment = this.renderByFileFormat(attachment);
 
     return (
       <Modal {...props} className="attachment-delete-modal" bsSize="large" aria-labelledby="contained-modal-title-lg">
@@ -77,11 +79,15 @@ export default class DeleteAttachmentModal extends React.Component {
           <div className="mr-3 d-inline-block">
             {deletingIndicator}
           </div>
-          <Button onClick={this._onDeleteConfirm} bsStyle="danger"
-            disabled={this.props.deleting}>Delete!</Button>
+          <Button
+            onClick={this._onDeleteConfirm}
+            bsStyle="danger"
+            disabled={this.props.deleting}
+          >Delete!
+          </Button>
         </Modal.Footer>
       </Modal>
     );
   }
-}
 
+}

+ 7 - 6
src/client/js/components/PageAttachment/PageAttachmentList.js

@@ -1,3 +1,4 @@
+/* eslint-disable react/prop-types */
 import React from 'react';
 
 import Attachment from './Attachment';
@@ -12,25 +13,25 @@ export default class PageAttachmentList extends React.Component {
     const attachmentList = this.props.attachments.map((attachment, idx) => {
       return (
         <Attachment
-          key={'page:attachment:' + attachment._id}
+          key={`page:attachment:${attachment._id}`}
           attachment={attachment}
           inUse={this.props.inUse[attachment._id] || false}
           onAttachmentDeleteClicked={this.props.onAttachmentDeleteClicked}
           isUserLoggedIn={this.props.isUserLoggedIn}
-         />
+        />
       );
     });
 
     return (
       <div>
-        {(attachmentList.length != 0) &&
-          <h5><strong>Attachments</strong></h5>
+        {(attachmentList.length !== 0)
+          && <h5><strong>Attachments</strong></h5>
         }
-        <ul className='pl-2'>
+        <ul className="pl-2">
           {attachmentList}
         </ul>
       </div>
     );
   }
-}
 
+}

+ 22 - 20
src/client/js/components/PageComment/Comment.js

@@ -41,9 +41,8 @@ export default class Comment extends React.Component {
     this.renderHtml(nextProps.comment.comment);
   }
 
-  //not used
+  // not used
   setMarkdown(markdown) {
-    this.setState({ markdown });
     this.renderHtml(markdown);
   }
 
@@ -56,13 +55,13 @@ export default class Comment extends React.Component {
   }
 
   getRootClassName() {
-    return 'page-comment '
-        + (this.isCurrentUserEqualsToAuthor() ? 'page-comment-me ' : '');
+    return `page-comment ${
+      this.isCurrentUserEqualsToAuthor() ? 'page-comment-me ' : ''}`;
   }
 
   getRevisionLabelClassName() {
-    return 'page-comment-revision label '
-        + (this.isCurrentRevision() ? 'label-primary' : 'label-default');
+    return `page-comment-revision label ${
+      this.isCurrentRevision() ? 'label-primary' : 'label-default'}`;
   }
 
   deleteBtnClickedHandler() {
@@ -73,10 +72,12 @@ export default class Comment extends React.Component {
     const config = this.props.crowi.getConfig();
     const isMathJaxEnabled = !!config.env.MATHJAX;
     return (
-      <RevisionBody html={this.state.html}
-          isMathJaxEnabled={isMathJaxEnabled}
-          renderMathJaxOnInit={true}
-          additionalClassName="comment" />
+      <RevisionBody
+        html={this.state.html}
+        isMathJaxEnabled={isMathJaxEnabled}
+        renderMathJaxOnInit
+        additionalClassName="comment"
+      />
     );
   }
 
@@ -88,26 +89,26 @@ export default class Comment extends React.Component {
     const crowiRenderer = this.props.crowiRenderer;
     const interceptorManager = this.props.crowi.interceptorManager;
     interceptorManager.process('preRenderComment', context)
-      .then(() => interceptorManager.process('prePreProcess', context))
+      .then(() => { return interceptorManager.process('prePreProcess', context) })
       .then(() => {
         context.markdown = crowiRenderer.preProcess(context.markdown);
       })
-      .then(() => interceptorManager.process('postPreProcess', context))
+      .then(() => { return interceptorManager.process('postPreProcess', context) })
       .then(() => {
-        var parsedHTML = crowiRenderer.process(context.markdown);
-        context['parsedHTML'] = parsedHTML;
+        const parsedHTML = crowiRenderer.process(context.markdown);
+        context.parsedHTML = parsedHTML;
       })
-      .then(() => interceptorManager.process('prePostProcess', context))
+      .then(() => { return interceptorManager.process('prePostProcess', context) })
       .then(() => {
         context.parsedHTML = crowiRenderer.postProcess(context.parsedHTML);
       })
-      .then(() => interceptorManager.process('postPostProcess', context))
-      .then(() => interceptorManager.process('preRenderCommentHtml', context))
+      .then(() => { return interceptorManager.process('postPostProcess', context) })
+      .then(() => { return interceptorManager.process('preRenderCommentHtml', context) })
       .then(() => {
         this.setState({ html: context.parsedHTML });
       })
       // process interceptors for post rendering
-      .then(() => interceptorManager.process('postRenderCommentHtml', context));
+      .then(() => { return interceptorManager.process('postRenderCommentHtml', context) });
 
   }
 
@@ -118,7 +119,7 @@ export default class Comment extends React.Component {
 
     const rootClassName = this.getRootClassName();
     const commentDate = dateFnsFormat(comment.createdAt, 'YYYY/MM/DD HH:mm');
-    const commentBody = isMarkdown ? this.renderRevisionBody(): ReactUtils.nl2br(comment.comment);
+    const commentBody = isMarkdown ? this.renderRevisionBody() : ReactUtils.nl2br(comment.comment);
     const creatorsPage = `/user/${creator.username}`;
     const revHref = `?revision=${comment.revision}`;
     const revFirst8Letters = comment.revision.substr(-8);
@@ -139,7 +140,7 @@ export default class Comment extends React.Component {
             <a className={revisionLavelClassName} href={revHref}>{revFirst8Letters}</a>
           </div>
           <div className="page-comment-control">
-            <button className="btn btn-link" onClick={this.deleteBtnClickedHandler}>
+            <button type="button" className="btn btn-link" onClick={this.deleteBtnClickedHandler}>
               <i className="ti-close"></i>
             </button>
           </div>
@@ -147,6 +148,7 @@ export default class Comment extends React.Component {
       </div>
     );
   }
+
 }
 
 Comment.propTypes = {

+ 7 - 8
src/client/js/components/PageComment/CommentPreview.js

@@ -8,17 +8,15 @@ import RevisionBody from '../Page/RevisionBody';
  */
 export default class CommentPreview extends React.Component {
 
-  constructor(props) {
-    super(props);
-  }
-
   render() {
     return (
-      <div className="page-comment-preview-body"
-          ref={(elm) => {
+      <div
+        className="page-comment-preview-body"
+        ref={(elm) => {
             this.previewElement = elm;
             this.props.inputRef(elm);
-          }}>
+          }}
+      >
 
         <RevisionBody
           {...this.props}
@@ -27,9 +25,10 @@ export default class CommentPreview extends React.Component {
       </div>
     );
   }
+
 }
 
 CommentPreview.propTypes = {
   html: PropTypes.string,
-  inputRef: PropTypes.func.isRequired,  // for getting div element
+  inputRef: PropTypes.func.isRequired, // for getting div element
 };

+ 2 - 6
src/client/js/components/PageComment/DeleteCommentModal.js

@@ -16,10 +16,6 @@ export default class DeleteCommentModal extends React.Component {
    */
   static get OMIT_BODY_THRES() { return 400 }
 
-  constructor(props) {
-    super(props);
-  }
-
   componentWillMount() {
   }
 
@@ -34,7 +30,7 @@ export default class DeleteCommentModal extends React.Component {
     // generate body
     let commentBody = comment.comment;
     if (commentBody.length > DeleteCommentModal.OMIT_BODY_THRES) { // omit
-      commentBody = commentBody.substr(0, DeleteCommentModal.OMIT_BODY_THRES) + '...';
+      commentBody = `${commentBody.substr(0, DeleteCommentModal.OMIT_BODY_THRES)}...`;
     }
     commentBody = ReactUtils.nl2br(commentBody);
 
@@ -68,6 +64,6 @@ DeleteCommentModal.propTypes = {
   isShown: PropTypes.bool.isRequired,
   comment: PropTypes.object,
   errorMessage: PropTypes.string,
-  cancel: PropTypes.func.isRequired,            // for cancel evnet handling
+  cancel: PropTypes.func.isRequired, // for cancel evnet handling
   confirmedToDelete: PropTypes.func.isRequired, // for confirmed event handling
 };

+ 34 - 26
src/client/js/components/PageComments.js

@@ -1,3 +1,4 @@
+/* eslint-disable react/no-access-state-in-setstate */
 import React from 'react';
 import PropTypes from 'prop-types';
 
@@ -32,7 +33,7 @@ export default class PageComments extends React.Component {
       errorMessageForDeleting: undefined,
     };
 
-    this.growiRenderer = new GrowiRenderer(this.props.crowi, this.props.crowiOriginRenderer, {mode: 'comment'});
+    this.growiRenderer = new GrowiRenderer(this.props.crowi, this.props.crowiOriginRenderer, { mode: 'comment' });
 
     this.init = this.init.bind(this);
     this.confirmToDeleteComment = this.confirmToDeleteComment.bind(this);
@@ -51,8 +52,8 @@ export default class PageComments extends React.Component {
       return;
     }
 
-    const layoutType = this.props.crowi.getConfig()['layoutType'];
-    this.setState({isLayoutTypeGrowi: 'crowi-plus' === layoutType || 'growi' === layoutType});
+    const layoutType = this.props.crowi.getConfig().layoutType;
+    this.setState({ isLayoutTypeGrowi: layoutType === 'crowi-plus' || layoutType === 'growi' });
 
     this.retrieveData();
   }
@@ -62,35 +63,36 @@ export default class PageComments extends React.Component {
    */
   retrieveData() {
     // get data (desc order array)
-    this.props.crowi.apiGet('/comments.get', {page_id: this.props.pageId})
-      .then(res => {
+    this.props.crowi.apiGet('/comments.get', { page_id: this.props.pageId })
+      .then((res) => {
         if (res.ok) {
-          this.setState({comments: res.comments});
+          this.setState({ comments: res.comments });
         }
       });
   }
 
   confirmToDeleteComment(comment) {
-    this.setState({commentToDelete: comment});
+    this.setState({ commentToDelete: comment });
     this.showDeleteConfirmModal();
   }
 
   deleteComment() {
     const comment = this.state.commentToDelete;
 
-    this.props.crowi.apiPost('/comments.remove', {comment_id: comment._id})
-    .then(res => {
-      if (res.ok) {
-        this.findAndSplice(comment);
-      }
-      this.closeDeleteConfirmModal();
-    }).catch(err => {
-      this.setState({errorMessageForDeleting: err.message});
-    });
+    this.props.crowi.apiPost('/comments.remove', { comment_id: comment._id })
+      .then((res) => {
+        if (res.ok) {
+          this.findAndSplice(comment);
+        }
+        this.closeDeleteConfirmModal();
+      })
+      .catch((err) => {
+        this.setState({ errorMessageForDeleting: err.message });
+      });
   }
 
   findAndSplice(comment) {
-    let comments = this.state.comments;
+    const comments = this.state.comments;
 
     const index = comments.indexOf(comment);
     if (index < 0) {
@@ -98,11 +100,11 @@ export default class PageComments extends React.Component {
     }
     comments.splice(index, 1);
 
-    this.setState({comments});
+    this.setState({ comments });
   }
 
   showDeleteConfirmModal() {
-    this.setState({isDeleteConfirmModalShown: true});
+    this.setState({ isDeleteConfirmModalShown: true });
   }
 
   closeDeleteConfirmModal() {
@@ -123,35 +125,40 @@ export default class PageComments extends React.Component {
   generateCommentElements(comments) {
     return comments.map((comment) => {
       return (
-        <Comment key={comment._id} comment={comment}
+        <Comment
+          key={comment._id}
+          comment={comment}
           currentUserId={this.props.crowi.me}
           currentRevisionId={this.props.revisionId}
           deleteBtnClicked={this.confirmToDeleteComment}
           crowi={this.props.crowi}
-          crowiRenderer={this.growiRenderer} />
+          crowiRenderer={this.growiRenderer}
+        />
       );
     });
   }
 
   render() {
-    let currentComments = [];
-    let newerComments = [];
-    let olderComments = [];
+    const currentComments = [];
+    const newerComments = [];
+    const olderComments = [];
 
     let comments = this.state.comments;
     if (this.state.isLayoutTypeGrowi) {
       // replace with asc order array
-      comments = comments.slice().reverse();  // non-destructive reverse
+      comments = comments.slice().reverse(); // non-destructive reverse
     }
 
     // divide by revisionId and createdAt
     const revisionId = this.props.revisionId;
     const revisionCreatedAt = this.props.revisionCreatedAt;
     comments.forEach((comment) => {
+      // comparing ObjectId
+      // eslint-disable-next-line eqeqeq
       if (comment.revision == revisionId) {
         currentComments.push(comment);
       }
-      else if (Date.parse(comment.createdAt)/1000 > revisionCreatedAt) {
+      else if (Date.parse(comment.createdAt) / 1000 > revisionCreatedAt) {
         newerComments.push(comment);
       }
       else {
@@ -237,6 +244,7 @@ export default class PageComments extends React.Component {
       </div>
     );
   }
+
 }
 
 PageComments.propTypes = {

+ 30 - 23
src/client/js/components/PageEditor.js

@@ -3,13 +3,13 @@ import PropTypes from 'prop-types';
 
 import { throttle, debounce } from 'throttle-debounce';
 
+import * as toastr from 'toastr';
 import GrowiRenderer from '../util/GrowiRenderer';
 
 import { EditorOptions, PreviewOptions } from './PageEditor/OptionsSelector';
 import Editor from './PageEditor/Editor';
 import Preview from './PageEditor/Preview';
 import scrollSyncHelper from './PageEditor/ScrollSyncHelper';
-import * as toastr from 'toastr';
 
 
 export default class PageEditor extends React.Component {
@@ -33,7 +33,7 @@ export default class PageEditor extends React.Component {
       previewOptions: this.props.previewOptions,
     };
 
-    this.growiRenderer = new GrowiRenderer(this.props.crowi, this.props.crowiRenderer, {mode: 'editor'});
+    this.growiRenderer = new GrowiRenderer(this.props.crowi, this.props.crowiRenderer, { mode: 'editor' });
 
     this.setCaretLine = this.setCaretLine.bind(this);
     this.focusToEditor = this.focusToEditor.bind(this);
@@ -71,12 +71,12 @@ export default class PageEditor extends React.Component {
   setMarkdown(markdown, updateEditorValue = true) {
     this.setState({ markdown });
     if (updateEditorValue) {
-      this.refs.editor.setValue(markdown);
+      this.editor.setValue(markdown);
     }
   }
 
   focusToEditor() {
-    this.refs.editor.forceToFocus();
+    this.editor.forceToFocus();
   }
 
   /**
@@ -84,7 +84,7 @@ export default class PageEditor extends React.Component {
    * @param {number} line
    */
   setCaretLine(line) {
-    this.refs.editor.setCaretLine(line);
+    this.editor.setCaretLine(line);
     scrollSyncHelper.scrollPreview(this.previewElement, line);
   }
 
@@ -119,7 +119,7 @@ export default class PageEditor extends React.Component {
    */
   async onUpload(file) {
     try {
-      let res  = await this.props.crowi.apiGet('/attachments.limit', {_csrf: this.props.crowi.csrfToken, fileSize: file.size});
+      let res = await this.props.crowi.apiGet('/attachments.limit', { _csrf: this.props.crowi.csrfToken, fileSize: file.size });
       if (!res.isUploadable) {
         toastr.error(undefined, 'MongoDB for uploading files reaches limit', {
           closeButton: true,
@@ -149,9 +149,9 @@ export default class PageEditor extends React.Component {
       // when image
       if (attachment.fileFormat.startsWith('image/')) {
         // modify to "![fileName](url)" syntax
-        insertText = '!' + insertText;
+        insertText = `!${insertText}`;
       }
-      this.refs.editor.insertText(insertText);
+      this.editor.insertText(insertText);
 
       // when if created newly
       if (res.pageCreated) {
@@ -162,13 +162,14 @@ export default class PageEditor extends React.Component {
       this.apiErrorHandler(e);
     }
     finally {
-      this.refs.editor.terminateUploadingState();
+      this.editor.terminateUploadingState();
     }
   }
 
   /**
    * the scroll event handler from codemirror
-   * @param {any} data {left, top, width, height, clientWidth, clientHeight} object that represents the current scroll position, the size of the scrollable area, and the size of the visible area (minus scrollbars).
+   * @param {any} data {left, top, width, height, clientWidth, clientHeight} object that represents the current scroll position,
+   *                    the size of the scrollable area, and the size of the visible area (minus scrollbars).
    *                    And data.line is also available that is added by Editor component
    * @see https://codemirror.net/doc/manual.html#events
    */
@@ -253,13 +254,13 @@ export default class PageEditor extends React.Component {
 
     // prevent circular invocation
     if (this.isOriginOfScrollSyncEditor) {
-      this.isOriginOfScrollSyncEditor = false;  // turn off the flag
+      this.isOriginOfScrollSyncEditor = false; // turn off the flag
       return;
     }
 
     // turn on the flag
     this.isOriginOfScrollSyncPreview = true;
-    scrollSyncHelper.scrollEditor(this.refs.editor, this.previewElement, offset);
+    scrollSyncHelper.scrollEditor(this.editor, this.previewElement, offset);
   }
 
   saveDraft() {
@@ -268,6 +269,7 @@ export default class PageEditor extends React.Component {
       this.props.crowi.saveDraft(this.props.pagePath, this.state.markdown);
     }
   }
+
   clearDraft() {
     this.props.crowi.clearDraft(this.props.pagePath);
   }
@@ -278,32 +280,32 @@ export default class PageEditor extends React.Component {
     // render html
     const context = {
       markdown: this.state.markdown,
-      currentPagePath: decodeURIComponent(location.pathname)
+      currentPagePath: decodeURIComponent(window.location.pathname),
     };
 
     const growiRenderer = this.growiRenderer;
     const interceptorManager = this.props.crowi.interceptorManager;
     interceptorManager.process('preRenderPreview', context)
-      .then(() => interceptorManager.process('prePreProcess', context))
+      .then(() => { return interceptorManager.process('prePreProcess', context) })
       .then(() => {
         context.markdown = growiRenderer.preProcess(context.markdown);
       })
-      .then(() => interceptorManager.process('postPreProcess', context))
+      .then(() => { return interceptorManager.process('postPreProcess', context) })
       .then(() => {
         const parsedHTML = growiRenderer.process(context.markdown);
-        context['parsedHTML'] = parsedHTML;
+        context.parsedHTML = parsedHTML;
       })
-      .then(() => interceptorManager.process('prePostProcess', context))
+      .then(() => { return interceptorManager.process('prePostProcess', context) })
       .then(() => {
         context.parsedHTML = growiRenderer.postProcess(context.parsedHTML);
       })
-      .then(() => interceptorManager.process('postPostProcess', context))
-      .then(() => interceptorManager.process('preRenderPreviewHtml', context))
+      .then(() => { return interceptorManager.process('postPostProcess', context) })
+      .then(() => { return interceptorManager.process('preRenderPreviewHtml', context) })
       .then(() => {
         this.setState({ html: context.parsedHTML });
       })
       // process interceptors for post rendering
-      .then(() => interceptorManager.process('postRenderPreviewHtml', context));
+      .then(() => { return interceptorManager.process('postRenderPreviewHtml', context) });
 
   }
 
@@ -326,7 +328,9 @@ export default class PageEditor extends React.Component {
     return (
       <div className="row">
         <div className="col-md-6 col-sm-12 page-editor-editor-container">
-          <Editor ref="editor" value={this.state.markdown}
+          <Editor
+            ref={(c) => { this.editor = c }}
+            value={this.state.markdown}
             editorOptions={this.state.editorOptions}
             noCdn={noCdn}
             isMobile={this.props.crowi.isMobile}
@@ -343,8 +347,10 @@ export default class PageEditor extends React.Component {
           />
         </div>
         <div className="col-md-6 hidden-sm hidden-xs page-editor-preview-container">
-          <Preview html={this.state.html}
-            inputRef={el => this.previewElement = el}
+          <Preview
+            html={this.state.html}
+            // eslint-disable-next-line no-return-assign
+            inputRef={(el) => { return this.previewElement = el }}
             isMathJaxEnabled={this.state.isMathJaxEnabled}
             renderMathJaxOnInit={false}
             previewOptions={this.state.previewOptions}
@@ -354,6 +360,7 @@ export default class PageEditor extends React.Component {
       </div>
     );
   }
+
 }
 
 PageEditor.propTypes = {

+ 2 - 2
src/client/js/components/PageEditor/AbstractEditor.js

@@ -116,11 +116,12 @@ export default class AbstractEditor extends React.Component {
   getNavbarItems() {
     return null;
   }
+
 }
 
 AbstractEditor.propTypes = {
   value: PropTypes.string,
-  ifGfmMode: PropTypes.bool,
+  isGfmMode: PropTypes.bool,
   editorOptions: PropTypes.object,
   onChange: PropTypes.func,
   onScroll: PropTypes.func,
@@ -133,4 +134,3 @@ AbstractEditor.propTypes = {
 AbstractEditor.defaultProps = {
   isGfmMode: true,
 };
-

+ 20 - 16
src/client/js/components/PageEditor/Cheatsheet.js

@@ -1,8 +1,11 @@
+/* eslint-disable max-len */
+
 import React from 'react';
 import PropTypes from 'prop-types';
 import { translate } from 'react-i18next';
 
 class Cheatsheet extends React.Component {
+
   render() {
     const { t } = this.props;
 
@@ -11,9 +14,9 @@ class Cheatsheet extends React.Component {
         <div className="col-sm-6">
           <h4>{t('sandbox.header')}</h4>
           <ul className="hljs">
-            <li><code># </code>{t('sandbox.header_x', {index: '1'})}</li>
-            <li><code>## </code>{t('sandbox.header_x', {index: '2'})}</li>
-            <li><code>### </code>{t('sandbox.header_x', {index: '3'})}</li>
+            <li><code># </code>{t('sandbox.header_x', { index: '1' })}</li>
+            <li><code>## </code>{t('sandbox.header_x', { index: '2' })}</li>
+            <li><code>### </code>{t('sandbox.header_x', { index: '3' })}</li>
           </ul>
           <h4>{t('sandbox.block')}</h4>
           <p className="mb-1"><code>[{t('sandbox.empty_line')}]</code>{t('sandbox.block_detail')}</p>
@@ -33,7 +36,7 @@ class Cheatsheet extends React.Component {
             <li><i>*{t('sandbox.italics')}*</i></li>
             <li><b>**{t('sandbox.bold')}**</b></li>
             <li><i><b>***{t('sandbox.italic_bold')}***</b></i></li>
-            <li>~~{t('sandbox.strikethrough')}~~ => <s>{t('sandbox.strikethrough')}</s></li>
+            <li>~~{t('sandbox.strikethrough')}~~ =&lt; <s>{t('sandbox.strikethrough')}</s></li>
           </ul>
           <h4>{t('sandbox.link')}</h4>
           <ul className="hljs">
@@ -50,13 +53,13 @@ class Cheatsheet extends React.Component {
         <div className="col-sm-6">
           <h4>{t('sandbox.list')}</h4>
           <ul className="hljs">
-            <li>- {t('sandbox.unordered_list_x', {index: '1'})}</li>
-            <li>&nbsp;&nbsp;- {t('sandbox.unordered_list_x', {index: '1.1'})}</li>
-            <li>- {t('sandbox.unordered_list_x', {index: '2'})}</li>
+            <li>- {t('sandbox.unordered_list_x', { index: '1' })}</li>
+            <li>&nbsp;&nbsp;- {t('sandbox.unordered_list_x', { index: '1.1' })}</li>
+            <li>- {t('sandbox.unordered_list_x', { index: '2' })}</li>
           </ul>
           <ul className="hljs">
-            <li>1. {t('sandbox.ordered_list_x', {index: '1'})}</li>
-            <li>1. {t('sandbox.ordered_list_x', {index: '2'})}</li>
+            <li>1. {t('sandbox.ordered_list_x', { index: '1' })}</li>
+            <li>1. {t('sandbox.ordered_list_x', { index: '2' })}</li>
           </ul>
           <ul className="hljs">
             <li>- [ ] {t('sandbox.task')}({t('sandbox.task_unchecked')})</li>
@@ -64,13 +67,13 @@ class Cheatsheet extends React.Component {
           </ul>
           <h4>{t('sandbox.quote')}</h4>
           <ul className="hljs">
-            <li>> {t('sandbox.quote1')}</li>
-            <li>> {t('sandbox.quote2')}</li>
+            <li>&gt; {t('sandbox.quote1')}</li>
+            <li>&gt; {t('sandbox.quote2')}</li>
           </ul>
           <ul className="hljs">
-            <li>>> {t('sandbox.quote_nested')}</li>
-            <li>>>> {t('sandbox.quote_nested')}</li>
-            <li>>>>> {t('sandbox.quote_nested')}</li>
+            <li>&gt;&gt; {t('sandbox.quote_nested')}</li>
+            <li>&gt;&gt;&gt; {t('sandbox.quote_nested')}</li>
+            <li>&gt;&gt;&gt;&gt; {t('sandbox.quote_nested')}</li>
           </ul>
           <h4>{t('sandbox.table')}</h4>
           <ul className="hljs text-center">
@@ -87,16 +90,17 @@ class Cheatsheet extends React.Component {
 
           <hr />
           <a href="/Sandbox" className="btn btn-info btn-block" target="_blank">
-            <i className="icon-share-alt"/> {t('sandbox.open_sandbox')}
+            <i className="icon-share-alt" /> {t('sandbox.open_sandbox')}
           </a>
         </div>
       </div>
     );
   }
+
 }
 
 Cheatsheet.propTypes = {
-  t: PropTypes.func.isRequired,               // i18next
+  t: PropTypes.func.isRequired, // i18next
 };
 
 export default translate()(Cheatsheet);

+ 174 - 120
src/client/js/components/PageEditor/CodeMirrorEditor.js

@@ -3,14 +3,25 @@ import PropTypes from 'prop-types';
 
 import Modal from 'react-bootstrap/es/Modal';
 import Button from 'react-bootstrap/es/Button';
+import urljoin from 'url-join';
+import * as codemirror from 'codemirror';
+
+import { UnControlled as ReactCodeMirror } from 'react-codemirror2';
 
 import InterceptorManager from '@commons/service/interceptor-manager';
 
-import urljoin from 'url-join';
+import AbstractEditor from './AbstractEditor';
+import SimpleCheatsheet from './SimpleCheatsheet';
+import Cheatsheet from './Cheatsheet';
+import pasteHelper from './PasteHelper';
+import EmojiAutoCompleteHelper from './EmojiAutoCompleteHelper';
+import PreventMarkdownListInterceptor from './PreventMarkdownListInterceptor';
+import MarkdownTableInterceptor from './MarkdownTableInterceptor';
+import mtu from './MarkdownTableUtil';
+import HandsontableModal from './HandsontableModal';
+
 const loadScript = require('simple-load-script');
 const loadCssSync = require('load-css-file');
-
-import * as codemirror from 'codemirror';
 // set save handler
 codemirror.commands.save = (instance) => {
   if (instance.codeMirrorEditor != null) {
@@ -19,9 +30,6 @@ codemirror.commands.save = (instance) => {
 };
 // set CodeMirror instance as 'CodeMirror' so that CDN addons can reference
 window.CodeMirror = require('codemirror');
-
-
-import { UnControlled as ReactCodeMirror } from 'react-codemirror2';
 require('codemirror/addon/display/placeholder');
 require('codemirror/addon/edit/matchbrackets');
 require('codemirror/addon/edit/matchtags');
@@ -42,19 +50,6 @@ require('codemirror/addon/display/placeholder');
 require('codemirror/mode/gfm/gfm');
 require('../../util/codemirror/autorefresh.ext');
 
-import AbstractEditor from './AbstractEditor';
-
-import SimpleCheatsheet from './SimpleCheatsheet';
-import Cheatsheet from './Cheatsheet';
-
-import pasteHelper from './PasteHelper';
-import EmojiAutoCompleteHelper from './EmojiAutoCompleteHelper';
-
-import PreventMarkdownListInterceptor from './PreventMarkdownListInterceptor';
-import MarkdownTableInterceptor from './MarkdownTableInterceptor';
-import mtu from './MarkdownTableUtil';
-import HandsontableModal from './HandsontableModal';
-
 export default class CodeMirrorEditor extends AbstractEditor {
 
   constructor(props) {
@@ -110,14 +105,14 @@ export default class CodeMirrorEditor extends AbstractEditor {
       new MarkdownTableInterceptor(),
     ]);
 
-    this.loadedThemeSet = new Set(['eclipse', 'elegant']);   // themes imported in _vendor.scss
+    this.loadedThemeSet = new Set(['eclipse', 'elegant']); // themes imported in _vendor.scss
     this.loadedKeymapSet = new Set();
   }
 
   componentWillMount() {
     if (this.props.emojiStrategy != null) {
       this.emojiAutoCompleteHelper = new EmojiAutoCompleteHelper(this.props.emojiStrategy);
-      this.setState({isEnabledEmojiAutoComplete: true});
+      this.setState({ isEnabledEmojiAutoComplete: true });
     }
   }
 
@@ -137,7 +132,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
   }
 
   getCodeMirror() {
-    return this.refs.cm.editor;
+    return this.cm.editor;
   }
 
   /**
@@ -185,14 +180,14 @@ export default class CodeMirrorEditor extends AbstractEditor {
    * @inheritDoc
    */
   setCaretLine(line) {
-    if (isNaN(line)) {
+    if (Number.isNaN(line)) {
       return;
     }
 
     const editor = this.getCodeMirror();
     const linePosition = Math.max(0, line);
 
-    editor.setCursor({line: linePosition});   // leave 'ch' field as null/undefined to indicate the end of line
+    editor.setCursor({ line: linePosition }); // leave 'ch' field as null/undefined to indicate the end of line
     this.setScrollTopByLine(linePosition);
   }
 
@@ -200,13 +195,13 @@ export default class CodeMirrorEditor extends AbstractEditor {
    * @inheritDoc
    */
   setScrollTopByLine(line) {
-    if (isNaN(line)) {
+    if (Number.isNaN(line)) {
       return;
     }
 
     const editor = this.getCodeMirror();
     // get top position of the line
-    const top = editor.charCoords({line, ch: 0}, 'local').top;
+    const top = editor.charCoords({ line, ch: 0 }, 'local').top;
     editor.scrollTo(null, top);
   }
 
@@ -331,11 +326,11 @@ export default class CodeMirrorEditor extends AbstractEditor {
    */
   loadKeymapMode(keymapMode) {
     const loadCss = this.loadCss;
-    let scriptList = [];
-    let cssList = [];
+    const scriptList = [];
+    const cssList = [];
 
     // add dependencies
-    if (this.loadedKeymapSet.size == 0) {
+    if (this.loadedKeymapSet.size === 0) {
       const dialogScriptUrl = this.props.noCdn
         ? urljoin(this.cmNoCdnScriptRoot, 'codemirror-dialog.js')
         : urljoin(this.cmCdnRoot, 'addon/dialog/dialog.min.js');
@@ -394,14 +389,14 @@ export default class CodeMirrorEditor extends AbstractEditor {
     }
 
     const context = {
-      handlers: [],  // list of handlers which process enter key
+      handlers: [], // list of handlers which process enter key
       editor: this,
     };
 
     const interceptorManager = this.interceptorManager;
     interceptorManager.process('preHandleEnter', context)
       .then(() => {
-        if (context.handlers.length == 0) {
+        if (context.handlers.length === 0) {
           codemirror.commands.newlineAndIndentContinueMarkdownList(this.getCodeMirror());
         }
       });
@@ -432,13 +427,14 @@ export default class CodeMirrorEditor extends AbstractEditor {
     if (mtu.isEndOfLine(editor) && mtu.linePartOfTableRE.test(strFromBol)) {
       if (!hasCustomClass) {
         additionalClassSet.add(autoformatTableClass);
-        this.setState({additionalClassSet});
+        this.setState({ additionalClassSet });
       }
     }
     else {
+      // eslint-disable-next-line no-lonely-if
       if (hasCustomClass) {
         additionalClassSet.delete(autoformatTableClass);
-        this.setState({additionalClassSet});
+        this.setState({ additionalClassSet });
       }
     }
   }
@@ -479,16 +475,13 @@ export default class CodeMirrorEditor extends AbstractEditor {
 
   /**
    * update states which related to cheatsheet
-   * @param {boolean} isGfmMode (use state.isGfmMode if null is set)
-   * @param {string} value (get value from codemirror if null is set)
+   * @param {boolean} isGfmModeTmp (use state.isGfmMode if null is set)
+   * @param {string} valueTmp (get value from codemirror if null is set)
    */
-  updateCheatsheetStates(isGfmMode, value) {
-    if (isGfmMode == null) {
-      isGfmMode = this.state.isGfmMode;
-    }
-    if (value == null) {
-      value = this.getCodeMirror().getDoc().getValue();
-    }
+  updateCheatsheetStates(isGfmModeTmp, valueTmp) {
+    const isGfmMode = isGfmModeTmp || this.state.isGfmMode;
+    const value = valueTmp || this.getCodeMirror().getDoc().getValue();
+
     // update isSimpleCheatsheetShown, isCheatsheetModalButtonShown
     const isSimpleCheatsheetShown = isGfmMode && value.length === 0;
     const isCheatsheetModalButtonShown = isGfmMode && value.length > 0;
@@ -505,11 +498,13 @@ export default class CodeMirrorEditor extends AbstractEditor {
     };
 
     return this.state.isLoadingKeymap
-      ? <div className="overlay overlay-loading-keymap">
+      ? (
+        <div className="overlay overlay-loading-keymap">
           <span style={style} className="overlay-content">
             <div className="speeding-wheel d-inline-block"></div> Loading Keymap ...
           </span>
         </div>
+      )
       : '';
   }
 
@@ -523,27 +518,27 @@ export default class CodeMirrorEditor extends AbstractEditor {
 
   renderCheatsheetModalButton() {
     const showCheatsheetModal = () => {
-      this.setState({isCheatsheetModalShown: true});
+      this.setState({ isCheatsheetModalShown: true });
     };
 
     const hideCheatsheetModal = () => {
-      this.setState({isCheatsheetModalShown: false});
+      this.setState({ isCheatsheetModalShown: false });
     };
 
     return (
       <React.Fragment>
         <Modal className="modal-gfm-cheatsheet" show={this.state.isCheatsheetModalShown} onHide={() => { hideCheatsheetModal() }}>
           <Modal.Header closeButton>
-            <Modal.Title><i className="icon-fw icon-question"/>Markdown Help</Modal.Title>
+            <Modal.Title><i className="icon-fw icon-question" />Markdown Help</Modal.Title>
           </Modal.Header>
           <Modal.Body className="pt-1">
             { this.renderCheatsheetModalBody() }
           </Modal.Body>
         </Modal>
 
-        <a className="gfm-cheatsheet-modal-link text-muted small" onClick={() => { showCheatsheetModal() }}>
+        <button type="button" className="btn-link gfm-cheatsheet-modal-link text-muted small mr-3" onClick={() => { showCheatsheetModal() }}>
           <i className="icon-question" /> Markdown
-        </a>
+        </button>
       </React.Fragment>
     );
   }
@@ -590,8 +585,8 @@ export default class CodeMirrorEditor extends AbstractEditor {
       for (let i = startLineNum; i <= endLineNum; i++) {
         lines.push(prefix + cm.getDoc().getLine(i));
       }
-      const replacement = lines.join('\n') + '\n';
-      cm.getDoc().replaceRange(replacement, {line: startLineNum, ch: 0}, {line: endLineNum + 1, ch: 0});
+      const replacement = `${lines.join('\n')}\n`;
+      cm.getDoc().replaceRange(replacement, { line: startLineNum, ch: 0 }, { line: endLineNum + 1, ch: 0 });
 
       cm.setCursor(endLineNum, cm.getDoc().getLine(endLineNum).length);
       cm.focus();
@@ -611,69 +606,123 @@ export default class CodeMirrorEditor extends AbstractEditor {
     if (!line.startsWith('#')) {
       prefix += ' ';
     }
-    cm.getDoc().replaceRange(prefix, {line: lineNum, ch: 0}, {line: lineNum, ch: 0});
+    cm.getDoc().replaceRange(prefix, { line: lineNum, ch: 0 }, { line: lineNum, ch: 0 });
     cm.focus();
   }
 
   showHandsonTableHandler() {
-    this.refs.handsontableModal.show(mtu.getMarkdownTable(this.getCodeMirror()));
+    this.handsontableModal.show(mtu.getMarkdownTable(this.getCodeMirror()));
   }
 
   getNavbarItems() {
     // The following styles will be removed after creating icons for the editor navigation bar.
-    const paddingTopBottom54 = {'paddingTop': '6px', 'paddingBottom': '5px'};
-    const paddingBottom6 = {'paddingBottom': '7px'};
-    const fontSize18 = {'fontSize': '18px'};
+    const paddingTopBottom54 = { paddingTop: '6px', paddingBottom: '5px' };
+    const paddingBottom6 = { paddingBottom: '7px' };
+    const fontSize18 = { fontSize: '18px' };
 
     return [
-      <Button key='nav-item-bold' bsSize="small" title={'Bold'}
-              onClick={ this.createReplaceSelectionHandler('**', '**') }>
-        <i className={'fa fa-bold'}></i>
+      <Button
+        key="nav-item-bold"
+        bsSize="small"
+        title="Bold"
+        onClick={this.createReplaceSelectionHandler('**', '**')}
+      >
+        <i className="fa fa-bold"></i>
       </Button>,
-      <Button key='nav-item-italic' bsSize="small" title={'Italic'}
-              onClick={ this.createReplaceSelectionHandler('*', '*') }>
-        <i className={'fa fa-italic'}></i>
+      <Button
+        key="nav-item-italic"
+        bsSize="small"
+        title="Italic"
+        onClick={this.createReplaceSelectionHandler('*', '*')}
+      >
+        <i className="fa fa-italic"></i>
       </Button>,
-      <Button key='nav-item-strikethough' bsSize="small" title={'Strikethrough'}
-              onClick={ this.createReplaceSelectionHandler('~~', '~~') }>
-        <i className={'fa fa-strikethrough'}></i>
+      <Button
+        key="nav-item-strikethough"
+        bsSize="small"
+        title="Strikethrough"
+        onClick={this.createReplaceSelectionHandler('~~', '~~')}
+      >
+        <i className="fa fa-strikethrough"></i>
       </Button>,
-      <Button key='nav-item-header' bsSize="small" title={'Heading'}
-              onClick={ this.makeHeaderHandler }>
-        <i className={'fa fa-header'}></i>
+      <Button
+        key="nav-item-header"
+        bsSize="small"
+        title="Heading"
+        onClick={this.makeHeaderHandler}
+      >
+        <i className="fa fa-header"></i>
       </Button>,
-      <Button key='nav-item-code' bsSize="small" title={'Inline Code'}
-              onClick={ this.createReplaceSelectionHandler('`', '`') }>
-        <i className={'fa fa-code'}></i>
+      <Button
+        key="nav-item-code"
+        bsSize="small"
+        title="Inline Code"
+        onClick={this.createReplaceSelectionHandler('`', '`')}
+      >
+        <i className="fa fa-code"></i>
       </Button>,
-      <Button key='nav-item-quote' bsSize="small" title={'Quote'}
-              onClick={ this.createAddPrefixToEachLinesHandler('> ') } style={paddingBottom6}>
-        <i className={'ti-quote-right'}></i>
+      <Button
+        key="nav-item-quote"
+        bsSize="small"
+        title="Quote"
+        onClick={this.createAddPrefixToEachLinesHandler('> ')}
+        style={paddingBottom6}
+      >
+        <i className="ti-quote-right"></i>
       </Button>,
-      <Button key='nav-item-ul' bsSize="small" title={'List'}
-              onClick={ this.createAddPrefixToEachLinesHandler('- ') } style={paddingTopBottom54}>
-        <i className={'ti-list'} style={fontSize18}></i>
+      <Button
+        key="nav-item-ul"
+        bsSize="small"
+        title="List"
+        onClick={this.createAddPrefixToEachLinesHandler('- ')}
+        style={paddingTopBottom54}
+      >
+        <i className="ti-list" style={fontSize18}></i>
       </Button>,
-      <Button key='nav-item-ol' bsSize="small" title={'Numbered List'}
-              onClick={ this.createAddPrefixToEachLinesHandler('1. ') } style={paddingTopBottom54}>
-        <i className={'ti-list-ol'} style={fontSize18}></i>
+      <Button
+        key="nav-item-ol"
+        bsSize="small"
+        title="Numbered List"
+        onClick={this.createAddPrefixToEachLinesHandler('1. ')}
+        style={paddingTopBottom54}
+      >
+        <i className="ti-list-ol" style={fontSize18}></i>
       </Button>,
-      <Button key='nav-item-checkbox' bsSize="small" title={'Check List'}
-              onClick={ this.createAddPrefixToEachLinesHandler('- [ ] ') } style={paddingBottom6}>
-        <i className={'ti-check-box'}></i>
+      <Button
+        key="nav-item-checkbox"
+        bsSize="small"
+        title="Check List"
+        onClick={this.createAddPrefixToEachLinesHandler('- [ ] ')}
+        style={paddingBottom6}
+      >
+        <i className="ti-check-box"></i>
       </Button>,
-      <Button key='nav-item-link' bsSize="small" title={'Link'}
-              onClick={ this.createReplaceSelectionHandler('[', ']()') } style={paddingBottom6}>
-        <i className={'icon-link'}></i>
+      <Button
+        key="nav-item-link"
+        bsSize="small"
+        title="Link"
+        onClick={this.createReplaceSelectionHandler('[', ']()')}
+        style={paddingBottom6}
+      >
+        <i className="icon-link"></i>
       </Button>,
-      <Button key='nav-item-image' bsSize="small" title={'Image'}
-              onClick={ this.createReplaceSelectionHandler('![', ']()') } style={paddingBottom6}>
-        <i className={'icon-picture'}></i>
+      <Button
+        key="nav-item-image"
+        bsSize="small"
+        title="Image"
+        onClick={this.createReplaceSelectionHandler('![', ']()')}
+        style={paddingBottom6}
+      >
+        <i className="icon-picture"></i>
+      </Button>,
+      <Button
+        key="nav-item-table"
+        bsSize="small"
+        title="Table"
+        onClick={this.showHandsonTableHandler}
+      >
+        <img src="/images/icons/editor/table.svg" alt="icon-table" width="14" height="14" />
       </Button>,
-      <Button key='nav-item-table' bsSize="small" title={'Table'}
-              onClick={ this.showHandsonTableHandler }>
-        <img src="/images/icons/editor/table.svg" width="14" height="14" />
-      </Button>
     ];
   }
 
@@ -688,50 +737,51 @@ export default class CodeMirrorEditor extends AbstractEditor {
 
     const placeholder = this.state.isGfmMode ? 'Input with Markdown..' : 'Input with Plane Text..';
 
-    return <React.Fragment>
+    return (
+      <React.Fragment>
 
-      <ReactCodeMirror
-        ref="cm"
-        className={additionalClasses}
-        placeholder="search"
-        editorDidMount={(editor) => {
+        <ReactCodeMirror
+          ref={(c) => { this.cm = c }}
+          className={additionalClasses}
+          placeholder="search"
+          editorDidMount={(editor) => {
           // add event handlers
           editor.on('paste', this.pasteHandler);
           editor.on('scrollCursorIntoView', this.scrollCursorIntoViewHandler);
         }}
-        value={this.state.value}
-        options={{
-          mode: mode,
+          value={this.state.value}
+          options={{
+          mode,
           theme: editorOptions.theme,
           styleActiveLine: editorOptions.styleActiveLine,
           lineNumbers: this.props.lineNumbers,
           tabSize: 4,
           indentUnit: 4,
           lineWrapping: true,
-          autoRefresh: {force: true},   // force option is enabled by autorefresh.ext.js -- Yuki Takei
+          autoRefresh: { force: true }, // force option is enabled by autorefresh.ext.js -- Yuki Takei
           autoCloseTags: true,
-          placeholder: placeholder,
+          placeholder,
           matchBrackets: true,
-          matchTags: {bothTags: true},
+          matchTags: { bothTags: true },
           // folding
           foldGutter: this.props.lineNumbers,
           gutters: this.props.lineNumbers ? ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'] : [],
           // match-highlighter, matchesonscrollbar, annotatescrollbar options
-          highlightSelectionMatches: {annotateScrollbar: true},
+          highlightSelectionMatches: { annotateScrollbar: true },
           // markdown mode options
           highlightFormatting: true,
           // continuelist, indentlist
           extraKeys: {
-            'Enter': this.handleEnterKey,
+            Enter: this.handleEnterKey,
             'Ctrl-Enter': this.handleCtrlEnterKey,
             'Cmd-Enter': this.handleCtrlEnterKey,
-            'Tab': 'indentMore',
+            Tab: 'indentMore',
             'Shift-Tab': 'indentLess',
             'Ctrl-Q': (cm) => { cm.foldCode(cm.getCursor()) },
-          }
+          },
         }}
-        onCursor={this.cursorHandler}
-        onScroll={(editor, data) => {
+          onCursor={this.cursorHandler}
+          onScroll={(editor, data) => {
           if (this.props.onScroll != null) {
             // add line data
             const line = editor.lineAtHeight(data.top, 'local');
@@ -739,23 +789,27 @@ export default class CodeMirrorEditor extends AbstractEditor {
             this.props.onScroll(data);
           }
         }}
-        onChange={this.changeHandler}
-        onDragEnter={(editor, event) => {
+          onChange={this.changeHandler}
+          onDragEnter={(editor, event) => {
           if (this.props.onDragEnter != null) {
             this.props.onDragEnter(event);
           }
         }}
-      />
+        />
 
-      { this.renderLoadingKeymapOverlay() }
+        { this.renderLoadingKeymapOverlay() }
 
-      <div className="overlay overlay-gfm-cheatsheet mt-1 p-3 pt-3">
-        { this.state.isSimpleCheatsheetShown && this.renderSimpleCheatsheet() }
-        { this.state.isCheatsheetModalButtonShown && this.renderCheatsheetModalButton() }
-      </div>
+        <div className="overlay overlay-gfm-cheatsheet mt-1 p-3 pt-3">
+          { this.state.isSimpleCheatsheetShown && this.renderSimpleCheatsheet() }
+          { this.state.isCheatsheetModalButtonShown && this.renderCheatsheetModalButton() }
+        </div>
 
-      <HandsontableModal ref='handsontableModal' onSave={ table => mtu.replaceFocusedMarkdownTableWithEditor(this.getCodeMirror(), table) }/>
-    </React.Fragment>;
+        <HandsontableModal
+          ref={(c) => { this.handsontableModal = c }}
+          onSave={(table) => { return mtu.replaceFocusedMarkdownTableWithEditor(this.getCodeMirror(), table) }}
+        />
+      </React.Fragment>
+    );
   }
 
 }

+ 17 - 17
src/client/js/components/PageEditor/EmojiAutoCompleteHelper.js

@@ -14,8 +14,7 @@ class EmojiAutoCompleteHelper {
   }
 
   initEmojiImageMap() {
-    for (let unicode in this.emojiStrategy) {
-      const data = this.emojiStrategy[unicode];
+    for (const data of Object.values(this.emojiStrategy)) {
       const shortname = data.shortname;
       // add image tag
       this.emojiShortnameImageMap[shortname] = emojione.shortnameToImage(shortname);
@@ -62,7 +61,7 @@ class EmojiAutoCompleteHelper {
       // closeOnUnfocus: false,  // for debug
       hint: () => {
         const matched = editor.getDoc().getRange(sc.from(), sc.to());
-        const term = matched.replace(':', '');  // remove ':' in the head
+        const term = matched.replace(':', ''); // remove ':' in the head
 
         // get a list of shortnames
         const shortnames = this.searchEmojiShortnames(term);
@@ -87,10 +86,9 @@ class EmojiAutoCompleteHelper {
         text: shortname,
         className: 'crowi-emoji-autocomplete',
         render: (element) => {
-          element.innerHTML =
-            `<div class="img-container">${this.emojiShortnameImageMap[shortname]}</div>` +
-            `<span class="shortname-container">${shortname}</span>`;
-        }
+          element.innerHTML = `<div class="img-container">${this.emojiShortnameImageMap[shortname]}</div>`
+            + `<span class="shortname-container">${shortname}</span>`;
+        },
       };
     });
   }
@@ -103,16 +101,18 @@ class EmojiAutoCompleteHelper {
   searchEmojiShortnames(term) {
     const maxLength = 12;
 
-    let results1 = [], results2 = [], results3 = [], results4 = [];
-    const countLen1 = () => { results1.length };
-    const countLen2 = () => { countLen1() + results2.length };
-    const countLen3 = () => { countLen2() + results3.length };
-    const countLen4 = () => { countLen3() + results4.length };
+    const results1 = [];
+    const results2 = [];
+    const results3 = [];
+    const results4 = [];
+    const countLen1 = () => { return results1.length };
+    const countLen2 = () => { return countLen1() + results2.length };
+    const countLen3 = () => { return countLen2() + results3.length };
+    const countLen4 = () => { return countLen3() + results4.length };
+
     // TODO performance tune
     // when total length of all results is less than `maxLength`
-    for (let unicode in this.emojiStrategy) {
-      const data = this.emojiStrategy[unicode];
-
+    for (const data of Object.values(this.emojiStrategy)) {
       if (maxLength <= countLen1()) { break }
       // prefix match to shortname
       else if (data.shortname.indexOf(`:${term}`) > -1) {
@@ -127,13 +127,13 @@ class EmojiAutoCompleteHelper {
       }
       else if (maxLength <= countLen3()) { continue }
       // partial match to elements of aliases
-      else if ((data.aliases != null) && data.aliases.find(elem => elem.indexOf(term) > -1)) {
+      else if ((data.aliases != null) && data.aliases.find((elem) => { return elem.indexOf(term) > -1 })) {
         results3.push(data.shortname);
         continue;
       }
       else if (maxLength <= countLen4()) { continue }
       // partial match to elements of keywords
-      else if ((data.keywords != null) && data.keywords.find(elem => elem.indexOf(term) > -1)) {
+      else if ((data.keywords != null) && data.keywords.find((elem) => { return elem.indexOf(term) > -1 })) {
         results4.push(data.shortname);
       }
     }

+ 1 - 1
src/client/js/components/PageEditor/MarkdownListUtil.js

@@ -88,7 +88,7 @@ class MarkdownListUtil {
     // not listful data
     else {
       // append `indentAndMark` at the beginning of all lines (except the first line)
-      const replacedText = text.replace(/(\r\n|\r|\n)/g, '$1' + indentAndMark);
+      const replacedText = text.replace(/(\r\n|\r|\n)/g, `$1${indentAndMark}`);
       // append `indentAndMark` to the first line
       adjusted = indentAndMark + replacedText;
     }

+ 3 - 6
src/client/js/components/PageEditor/MarkdownTableInterceptor.js

@@ -8,10 +8,6 @@ import MarkdownTable from '../../models/MarkdownTable';
  */
 export default class MarkdownTableInterceptor extends BasicInterceptor {
 
-  constructor() {
-    super();
-  }
-
   /**
    * @inheritdoc
    */
@@ -32,8 +28,8 @@ export default class MarkdownTableInterceptor extends BasicInterceptor {
    * @inheritdoc
    */
   process(contextName, ...args) {
-    const context = Object.assign(args[0]);   // clone
-    const editor = context.editor;            // AbstractEditor instance
+    const context = Object.assign(args[0]); // clone
+    const editor = context.editor; // AbstractEditor instance
 
     // do nothing if editor is not a CodeMirrorEditor
     if (editor == null || editor.getCodeMirror() == null) {
@@ -67,4 +63,5 @@ export default class MarkdownTableInterceptor extends BasicInterceptor {
     // resolve
     return Promise.resolve(context);
   }
+
 }

+ 13 - 13
src/client/js/components/PageEditor/MarkdownTableUtil.js

@@ -9,7 +9,7 @@ class MarkdownTableUtil {
     // https://github.com/markdown-it/markdown-it/blob/d29f421927e93e88daf75f22089a3e732e195bd2/lib/rules_block/table.js#L83
     // https://regex101.com/r/7BN2fR/7
     this.tableAlignmentLineRE = /^[-:|][-:|\s]*$/;
-    this.tableAlignmentLineNegRE = /^[^-:]*$/;  // it is need to check to ignore empty row which is matched above RE
+    this.tableAlignmentLineNegRE = /^[^-:]*$/; // it is need to check to ignore empty row which is matched above RE
     this.linePartOfTableRE = /^\|[^\r\n]*|[^\r\n]*\|$|([^|\r\n]+\|[^|\r\n]*)+/; // own idea
 
     this.getBot = this.getBot.bind(this);
@@ -29,7 +29,7 @@ class MarkdownTableUtil {
   getBot(editor) {
     const curPos = editor.getCursor();
     if (!this.isInTable(editor)) {
-      return { line: curPos.line, ch: curPos.ch};
+      return { line: curPos.line, ch: curPos.ch };
     }
 
     const firstLine = editor.getDoc().firstLine();
@@ -51,7 +51,7 @@ class MarkdownTableUtil {
   getEot(editor) {
     const curPos = editor.getCursor();
     if (!this.isInTable(editor)) {
-      return { line: curPos.line, ch: curPos.ch};
+      return { line: curPos.line, ch: curPos.ch };
     }
 
     const lastLine = editor.getDoc().lastLine();
@@ -109,7 +109,7 @@ class MarkdownTableUtil {
    */
   isEndOfLine(editor) {
     const curPos = editor.getCursor();
-    return (curPos.ch == editor.getDoc().getLine(curPos.line).length);
+    return (curPos.ch === editor.getDoc().getLine(curPos.line).length);
   }
 
   /**
@@ -127,8 +127,8 @@ class MarkdownTableUtil {
    */
   addRowToMarkdownTable(mdtable) {
     const numCol = mdtable.table.length > 0 ? mdtable.table[0].length : 1;
-    let newRow = [];
-    (new Array(numCol)).forEach(() => newRow.push('')); // create cols
+    const newRow = [];
+    (new Array(numCol)).forEach(() => { return newRow.push('') }); // create cols
     mdtable.table.push(newRow);
   }
 
@@ -137,15 +137,14 @@ class MarkdownTableUtil {
    * (The merged markdown table options are used for the first markdown table.)
    * @param {Array} array of markdown table
    */
-  mergeMarkdownTable(mdtable_list) {
-    if (mdtable_list == undefined
-        || !(mdtable_list instanceof Array)) {
+  mergeMarkdownTable(mdtableList) {
+    if (mdtableList == null || !(mdtableList instanceof Array)) {
       return undefined;
     }
 
     let newTable = [];
-    const options = mdtable_list[0].options; // use option of first markdown-table
-    mdtable_list.forEach((mdtable) => {
+    const options = mdtableList[0].options; // use option of first markdown-table
+    mdtableList.forEach((mdtable) => {
       newTable = newTable.concat(mdtable.table);
     });
     return (new MarkdownTable(newTable, options));
@@ -176,15 +175,16 @@ class MarkdownTableUtil {
 
     let newMarkdown = '';
     if (markdownBeforeTable.length > 0) {
-      newMarkdown += markdownBeforeTable.join('\n') + '\n';
+      newMarkdown += `${markdownBeforeTable.join('\n')}\n`;
     }
     newMarkdown += table;
     if (markdownAfterTable.length > 0) {
-      newMarkdown += '\n' + markdownAfterTable.join('\n');
+      newMarkdown += `\n${markdownAfterTable.join('\n')}`;
     }
 
     return newMarkdown;
   }
+
 }
 
 // singleton pattern

+ 68 - 42
src/client/js/components/PageEditor/OptionsSelector.js

@@ -9,6 +9,28 @@ import ControlLabel from 'react-bootstrap/es/ControlLabel';
 import Dropdown from 'react-bootstrap/es/Dropdown';
 import MenuItem from 'react-bootstrap/es/MenuItem';
 
+export class EditorOptions {
+
+  constructor(props) {
+    this.theme = 'elegant';
+    this.keymapMode = 'default';
+    this.styleActiveLine = false;
+
+    Object.assign(this, props);
+  }
+
+}
+
+export class PreviewOptions {
+
+  constructor(props) {
+    this.renderMathJaxInRealtime = false;
+
+    Object.assign(this, props);
+  }
+
+}
+
 class OptionsSelector extends React.Component {
 
   constructor(props) {
@@ -25,7 +47,7 @@ class OptionsSelector extends React.Component {
     };
 
     this.availableThemes = [
-      'eclipse', 'elegant', 'neo', 'mdn-like', 'material', 'dracula', 'monokai', 'twilight'
+      'eclipse', 'elegant', 'neo', 'mdn-like', 'material', 'dracula', 'monokai', 'twilight',
     ];
     this.keymapModes = {
       default: 'Default',
@@ -52,8 +74,8 @@ class OptionsSelector extends React.Component {
 
   onChangeTheme() {
     const newValue = this.themeSelectorInputEl.value;
-    const newOpts = Object.assign(this.state.editorOptions, {theme: newValue});
-    this.setState({editorOptions: newOpts});
+    const newOpts = Object.assign(this.state.editorOptions, { theme: newValue });
+    this.setState({ editorOptions: newOpts });
 
     // dispatch event
     this.dispatchOnChange();
@@ -61,8 +83,8 @@ class OptionsSelector extends React.Component {
 
   onChangeKeymapMode() {
     const newValue = this.keymapModeSelectorInputEl.value;
-    const newOpts = Object.assign(this.state.editorOptions, {keymapMode: newValue});
-    this.setState({editorOptions: newOpts});
+    const newOpts = Object.assign(this.state.editorOptions, { keymapMode: newValue });
+    this.setState({ editorOptions: newOpts });
 
     // dispatch event
     this.dispatchOnChange();
@@ -73,8 +95,8 @@ class OptionsSelector extends React.Component {
     this._cddForceOpen = true;
 
     const newValue = !this.state.editorOptions.styleActiveLine;
-    const newOpts = Object.assign(this.state.editorOptions, {styleActiveLine: newValue});
-    this.setState({editorOptions: newOpts});
+    const newOpts = Object.assign(this.state.editorOptions, { styleActiveLine: newValue });
+    this.setState({ editorOptions: newOpts });
 
     // dispatch event
     this.dispatchOnChange();
@@ -85,8 +107,8 @@ class OptionsSelector extends React.Component {
     this._cddForceOpen = true;
 
     const newValue = !this.state.previewOptions.renderMathJaxInRealtime;
-    const newOpts = Object.assign(this.state.previewOptions, {renderMathJaxInRealtime: newValue});
-    this.setState({previewOptions: newOpts});
+    const newOpts = Object.assign(this.state.previewOptions, { renderMathJaxInRealtime: newValue });
+    this.setState({ previewOptions: newOpts });
 
     // dispatch event
     this.dispatchOnChange();
@@ -122,9 +144,15 @@ class OptionsSelector extends React.Component {
     return (
       <FormGroup controlId="formControlsSelect" className="my-0">
         <ControlLabel>Theme:</ControlLabel>
-        <FormControl componentClass="select" placeholder="select" bsClass={bsClassName} className="btn-group-sm selectpicker"
-            onChange={this.onChangeTheme}
-            inputRef={ el => this.themeSelectorInputEl=el }>
+        <FormControl
+          componentClass="select"
+          placeholder="select"
+          bsClass={bsClassName}
+          className="btn-group-sm selectpicker"
+          onChange={this.onChangeTheme}
+          // eslint-disable-next-line no-return-assign
+          inputRef={(el) => { return this.themeSelectorInputEl = el }}
+        >
 
           {optionElems}
 
@@ -135,13 +163,14 @@ class OptionsSelector extends React.Component {
 
   renderKeymapModeSelector() {
     const optionElems = [];
-    for (let mode in this.keymapModes) {
+    // eslint-disable-next-line guard-for-in, no-restricted-syntax
+    for (const mode in this.keymapModes) {
       const label = this.keymapModes[mode];
       const dataContent = (mode === 'default')
         ? label
         : `<img src='/images/icons/${mode}.png' width='16px' class='m-r-5'></img> ${label}`;
       optionElems.push(
-        <option key={mode} value={mode} data-content={dataContent}>{label}</option>
+        <option key={mode} value={mode} data-content={dataContent}>{label}</option>,
       );
     }
 
@@ -150,9 +179,15 @@ class OptionsSelector extends React.Component {
     return (
       <FormGroup controlId="formControlsSelect" className="my-0">
         <ControlLabel>Keymap:</ControlLabel>
-        <FormControl componentClass="select" placeholder="select" bsClass={bsClassName} className="btn-group-sm selectpicker"
-            onChange={this.onChangeKeymapMode}
-            inputRef={ el => this.keymapModeSelectorInputEl=el }>
+        <FormControl
+          componentClass="select"
+          placeholder="select"
+          bsClass={bsClassName}
+          className="btn-group-sm selectpicker"
+          onChange={this.onChangeKeymapMode}
+          // eslint-disable-next-line no-return-assign
+          inputRef={(el) => { return this.keymapModeSelectorInputEl = el }}
+        >
 
           {optionElems}
 
@@ -165,8 +200,13 @@ class OptionsSelector extends React.Component {
     return (
       <FormGroup controlId="formControlsSelect" className="my-0">
 
-        <Dropdown dropup id="configurationDropdown" className="configuration-dropdown"
-            open={this.state.isCddMenuOpened} onToggle={this.onToggleConfigurationDropdown}>
+        <Dropdown
+          dropup
+          id="configurationDropdown"
+          className="configuration-dropdown"
+          open={this.state.isCddMenuOpened}
+          onToggle={this.onToggleConfigurationDropdown}
+        >
 
           <Dropdown.Toggle bsSize="sm">
             <i className="icon-settings"></i>
@@ -219,7 +259,7 @@ class OptionsSelector extends React.Component {
 
     return (
       <MenuItem onClick={this.onClickRenderMathJaxInRealtime}>
-        <span className="icon-container"><img src="/images/icons/fx.svg" width="14px"></img></span>
+        <span className="icon-container"><img src="/images/icons/fx.svg" width="14px" alt="fx"></img></span>
         <span className="menuitem-label">MathJax Rendering</span>
         <i className={iconClassName}></i>
       </MenuItem>
@@ -227,34 +267,20 @@ class OptionsSelector extends React.Component {
   }
 
   render() {
-    return <div className="d-flex flex-row">
-      <span className="m-l-5">{this.renderThemeSelector()}</span>
-      <span className="m-l-5">{this.renderKeymapModeSelector()}</span>
-      <span className="m-l-5">{this.renderConfigurationDropdown()}</span>
-    </div>;
+    return (
+      <div className="d-flex flex-row">
+        <span className="m-l-5">{this.renderThemeSelector()}</span>
+        <span className="m-l-5">{this.renderKeymapModeSelector()}</span>
+        <span className="m-l-5">{this.renderConfigurationDropdown()}</span>
+      </div>
+    );
   }
-}
-
-export class EditorOptions {
-  constructor(props) {
-    this.theme = 'elegant';
-    this.keymapMode = 'default';
-    this.styleActiveLine = false;
 
-    Object.assign(this, props);
-  }
 }
 
-export class PreviewOptions {
-  constructor(props) {
-    this.renderMathJaxInRealtime = false;
-
-    Object.assign(this, props);
-  }
-}
 
 OptionsSelector.propTypes = {
-  t: PropTypes.func.isRequired,               // i18next
+  t: PropTypes.func.isRequired, // i18next
   crowi: PropTypes.object.isRequired,
   editorOptions: PropTypes.instanceOf(EditorOptions).isRequired,
   previewOptions: PropTypes.instanceOf(PreviewOptions).isRequired,

+ 3 - 1
src/client/js/components/PageEditor/PasteHelper.js

@@ -17,7 +17,7 @@ class PasteHelper {
     // get data in clipboard
     const text = event.clipboardData.getData('text/plain');
 
-    if (text.length == 0) {
+    if (text.length === 0) {
       return;
     }
 
@@ -35,6 +35,7 @@ class PasteHelper {
   fileAccepted(file, accept) {
     return file.type === 'application/x-moz-file' || accepts(file, accept);
   }
+
   /**
    * transplanted from react-dropzone
    * @see https://github.com/react-dropzone/react-dropzone/blob/master/src/utils/index.js
@@ -46,6 +47,7 @@ class PasteHelper {
   fileMatchSize(file, maxSize, minSize) {
     return file.size <= maxSize && file.size >= minSize;
   }
+
 }
 
 // singleton pattern

+ 3 - 6
src/client/js/components/PageEditor/PreventMarkdownListInterceptor.js

@@ -3,10 +3,6 @@ import mlu from './MarkdownListUtil';
 
 export default class PreventMarkdownListInterceptor extends BasicInterceptor {
 
-  constructor() {
-    super();
-  }
-
   /**
    * @inheritdoc
    */
@@ -27,8 +23,8 @@ export default class PreventMarkdownListInterceptor extends BasicInterceptor {
    * @inheritdoc
    */
   process(contextName, ...args) {
-    const context = Object.assign(args[0]);   // clone
-    const editor = context.editor;            // AbstractEditor instance
+    const context = Object.assign(args[0]); // clone
+    const editor = context.editor; // AbstractEditor instance
 
     // get strings from current position to EOL(end of line) before break the line
     const strToEol = editor.getStrToEol();
@@ -43,4 +39,5 @@ export default class PreventMarkdownListInterceptor extends BasicInterceptor {
     // resolve
     return Promise.resolve(context);
   }
+
 }

+ 8 - 9
src/client/js/components/PageEditor/Preview.js

@@ -10,24 +10,22 @@ import { PreviewOptions } from './OptionsSelector';
  */
 export default class Preview extends React.Component {
 
-  constructor(props) {
-    super(props);
-  }
-
   render() {
     const renderMathJaxInRealtime = this.props.previewOptions.renderMathJaxInRealtime;
 
     return (
-      <div className="page-editor-preview-body"
-          ref={(elm) => {
+      <div
+        className="page-editor-preview-body"
+        ref={(elm) => {
             this.previewElement = elm;
             this.props.inputRef(elm);
           }}
-          onScroll={(event) => {
+        onScroll={(event) => {
             if (this.props.onScroll != null) {
               this.props.onScroll(event.target.scrollTop);
             }
-          }}>
+          }}
+      >
 
         <RevisionBody
           {...this.props}
@@ -36,11 +34,12 @@ export default class Preview extends React.Component {
       </div>
     );
   }
+
 }
 
 Preview.propTypes = {
   html: PropTypes.string,
-  inputRef: PropTypes.func.isRequired,  // for getting div element
+  inputRef: PropTypes.func.isRequired, // for getting div element
   isMathJaxEnabled: PropTypes.bool,
   renderMathJaxOnInit: PropTypes.bool,
   previewOptions: PropTypes.instanceOf(PreviewOptions),

+ 38 - 36
src/client/js/components/PageEditor/ScrollSyncHelper.js

@@ -5,11 +5,8 @@
 class ScrollSyncHelper {
 
   /**
-	 * @typedef {{ element: Element, line: number }} CodeLineElement
-	 */
-
-  constructor() {
-  }
+   * @typedef {{ element: Element, line: number }} CodeLineElement
+   */
 
   getCodeLineElements(parentElement) {
     /** @type {CodeLineElement[]} */
@@ -17,26 +14,27 @@ class ScrollSyncHelper {
     if (!elements) {
       elements = Array.prototype.map.call(
         parentElement.getElementsByClassName('code-line'),
-        element => {
+        (element) => {
           const line = +element.getAttribute('data-line');
           return { element, line };
-        })
-        .filter(x => !isNaN(x.line));
+        },
+      )
+        .filter((x) => { return !Number.isNaN(x.line) });
     }
     return elements;
   }
 
   /**
-	 * Find the html elements that map to a specific target line in the editor.
-	 *
-	 * If an exact match, returns a single element. If the line is between elements,
-	 * returns the element prior to and the element after the given line.
-	 *
+   * Find the html elements that map to a specific target line in the editor.
+   *
+   * If an exact match, returns a single element. If the line is between elements,
+   * returns the element prior to and the element after the given line.
+   *
    * @param {Element} element
-	 * @param {number} targetLine
-	 *
-	 * @returns {{ previous: CodeLineElement, next?: CodeLineElement }}
-	 */
+   * @param {number} targetLine
+   *
+   * @returns {{ previous: CodeLineElement, next?: CodeLineElement }}
+   */
   getElementsForSourceLine(element, targetLine) {
     const lines = this.getCodeLineElements(element);
     let previous = lines[0] || null;
@@ -44,7 +42,7 @@ class ScrollSyncHelper {
       if (entry.line === targetLine) {
         return { previous: entry, next: null };
       }
-      else if (entry.line > targetLine) {
+      if (entry.line > targetLine) {
         return { previous, next: entry };
       }
       previous = entry;
@@ -53,13 +51,13 @@ class ScrollSyncHelper {
   }
 
   /**
-	 * Find the html elements that are at a specific pixel offset on the page.
-	 *
+   * Find the html elements that are at a specific pixel offset on the page.
+   *
    * @param {Element} parentElement
-	 * @param {number} offset
+   * @param {number} offset
    *
-	 * @returns {{ previous: CodeLineElement, next?: CodeLineElement }}
-	 */
+   * @returns {{ previous: CodeLineElement, next?: CodeLineElement }}
+   */
   getLineElementsAtPageOffset(parentElement, offset) {
     const lines = this.getCodeLineElements(parentElement);
 
@@ -82,7 +80,7 @@ class ScrollSyncHelper {
     if (hi >= 1 && hiElement.element.getBoundingClientRect().top > position) {
       const loElement = lines[lo];
       const bounds = loElement.element.getBoundingClientRect();
-      let previous = { element: loElement.element, line: loElement.line };
+      const previous = { element: loElement.element, line: loElement.line };
       if (bounds.height > 0) {
         previous.line += (position - bounds.top) / (bounds.height);
       }
@@ -99,12 +97,14 @@ class ScrollSyncHelper {
     const { previous, next } = this.getLineElementsAtPageOffset(parentElement, offset);
     if (previous) {
       if (next) {
-        const betweenProgress = (offset - parentElement.scrollTop - previous.element.getBoundingClientRect().top) / (next.element.getBoundingClientRect().top - previous.element.getBoundingClientRect().top);
+        const betweenProgress = (
+          offset - parentElement.scrollTop - previous.element.getBoundingClientRect().top)
+          / (next.element.getBoundingClientRect().top - previous.element.getBoundingClientRect().top);
         return previous.line + betweenProgress * (next.line - previous.line);
       }
-      else {
-        return previous.line;
-      }
+
+      return previous.line;
+
     }
     return null;
   }
@@ -123,11 +123,11 @@ class ScrollSyncHelper {
   }
 
   /**
-	 * Attempt to scroll preview element for a source line in the editor.
-	 *
+   * Attempt to scroll preview element for a source line in the editor.
+   *
    * @param {Element} previewElement
-	 * @param {number} line
-	 */
+   * @param {number} line
+   */
   scrollPreview(previewElement, line) {
     const { previous, next } = this.getElementsForSourceLine(previewElement, line);
     if (previous) {
@@ -149,12 +149,13 @@ class ScrollSyncHelper {
   }
 
   /**
-	 * Attempt to reveal the element that is overflowing from previewElement.
-	 *
+   * Attempt to reveal the element that is overflowing from previewElement.
+   *
    * @param {Element} previewElement
-	 * @param {number} line
-	 */
+   * @param {number} line
+   */
   scrollPreviewToRevealOverflowing(previewElement, line) {
+    // eslint-disable-next-line no-unused-vars
     const { previous, next } = this.getElementsForSourceLine(previewElement, line);
     if (previous) {
       const parentElementOffset = this.getParentElementOffset(previewElement);
@@ -191,6 +192,7 @@ class ScrollSyncHelper {
     line = Math.floor(line);
     editor.setScrollTopByLine(line);
   }
+
 }
 
 // singleton pattern

+ 10 - 8
src/client/js/components/PageEditor/SimpleCheatsheet.js

@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
 import { translate } from 'react-i18next';
 
 class SimpleCheatsheet extends React.Component {
+
   render() {
     const { t } = this.props;
 
@@ -12,8 +13,8 @@ class SimpleCheatsheet extends React.Component {
           <div className="row">
             <div className="col-xs-6">
               <p>
-                # {t('sandbox.header_x', {index: '1'})}<br />
-                ## {t('sandbox.header_x', {index: '2'})}
+                # {t('sandbox.header_x', { index: '1' })}<br />
+                ## {t('sandbox.header_x', { index: '2' })}
               </p>
               <p><i>*{t('sandbox.italics')}*</i>&nbsp;&nbsp;<b>**{t('sandbox.bold')}**</b></p>
               <p>
@@ -28,11 +29,11 @@ class SimpleCheatsheet extends React.Component {
             </div>
             <div className="col-xs-6">
               <p>
-                - {t('sandbox.unordered_list_x', {index: '1'})}<br />
-                &nbsp;&nbsp;&nbsp;- {t('sandbox.unordered_list_x', {index: '1.1'})}<br />
-                - {t('sandbox.unordered_list_x', {index: '2'})}<br />
-                1. {t('sandbox.ordered_list_x', {index: '1'})}<br />
-                1. {t('sandbox.ordered_list_x', {index: '2'})}
+                - {t('sandbox.unordered_list_x', { index: '1' })}<br />
+                &nbsp;&nbsp;&nbsp;- {t('sandbox.unordered_list_x', { index: '1.1' })}<br />
+                - {t('sandbox.unordered_list_x', { index: '2' })}<br />
+                1. {t('sandbox.ordered_list_x', { index: '1' })}<br />
+                1. {t('sandbox.ordered_list_x', { index: '2' })}
               </p>
               <hr />
               <p>[ ][ ] {t('sandbox.block_detail')}</p>
@@ -42,10 +43,11 @@ class SimpleCheatsheet extends React.Component {
       </div>
     );
   }
+
 }
 
 SimpleCheatsheet.propTypes = {
-  t: PropTypes.func.isRequired,               // i18next
+  t: PropTypes.func.isRequired, // i18next
 };
 
 export default translate()(SimpleCheatsheet);

+ 23 - 18
src/client/js/components/PageEditor/TextAreaEditor.js

@@ -79,18 +79,20 @@ export default class TextAreaEditor extends AbstractEditor {
    * @inheritDoc
    */
   setCaretLine(line) {
-    if (isNaN(line)) {
+    if (Number.isNaN(line)) {
       return;
     }
 
     // scroll to bottom
     this.textarea.scrollTop = this.textarea.scrollHeight;
 
-    const lines = this.textarea.value.split('\n').slice(0, line+1);
+    const lines = this.textarea.value.split('\n').slice(0, line + 1);
+    /* eslint-disable no-param-reassign, no-return-assign */
     const pos = lines
-        .map(lineStr => lineStr.length + 1) // correct length+1 of each lines
-        .reduce((a, x) => a += x, 0)        // sum
-        - 1;                                // -1
+      .map((lineStr) => { return lineStr.length + 1 }) // correct length+1 of each lines
+      .reduce((a, x) => { return a += x }, 0) //          sum
+        - 1; //                                           -1
+    /* eslint-enable no-param-reassign, no-return-assign */
 
     this.textarea.setSelectionRange(pos, pos);
   }
@@ -149,13 +151,13 @@ export default class TextAreaEditor extends AbstractEditor {
 
   getBolPos() {
     const currentPos = this.textarea.selectionStart;
-    return this.textarea.value.lastIndexOf('\n', currentPos-1) + 1;
+    return this.textarea.value.lastIndexOf('\n', currentPos - 1) + 1;
   }
 
   getEolPos() {
     const currentPos = this.textarea.selectionStart;
     const pos = this.textarea.value.indexOf('\n', currentPos);
-    if (pos < 0) {  // not found but EOF
+    if (pos < 0) { // not found but EOF
       return this.textarea.value.length;
     }
     return pos;
@@ -197,7 +199,7 @@ export default class TextAreaEditor extends AbstractEditor {
     }
 
     const context = {
-      handlers: [],  // list of handlers which process enter key
+      handlers: [], // list of handlers which process enter key
       editor: this,
     };
 
@@ -205,7 +207,7 @@ export default class TextAreaEditor extends AbstractEditor {
     interceptorManager.process('preHandleEnter', context)
       .then(() => {
         event.preventDefault();
-        if (context.handlers.length == 0) {
+        if (context.handlers.length === 0) {
           mlu.newlineAndIndentContinueMarkdownList(this);
         }
       });
@@ -240,21 +242,24 @@ export default class TextAreaEditor extends AbstractEditor {
   }
 
   render() {
-    return <React.Fragment>
-      <FormControl
-        componentClass="textarea" className="textarea-editor"
-        inputRef={ref => { this.textarea = ref }}
-        defaultValue={this.state.value}
-        onChange={(e) => {
+    return (
+      <React.Fragment>
+        <FormControl
+          componentClass="textarea"
+          className="textarea-editor"
+          inputRef={(ref) => { this.textarea = ref }}
+          defaultValue={this.state.value}
+          onChange={(e) => {
           if (this.props.onChange != null) {
             this.props.onChange(e.target.value);
           }
-        }} />
-    </React.Fragment>;
+        }}
+        />
+      </React.Fragment>
+    );
   }
 
 }
 
 TextAreaEditor.propTypes = Object.assign({
 }, AbstractEditor.propTypes);
-

+ 62 - 57
src/client/js/components/PageHistory.js

@@ -22,52 +22,55 @@ class PageHistory extends React.Component {
     const pageId = this.props.pageId;
 
     if (!pageId) {
-      return ;
+      return;
     }
 
-    this.props.crowi.apiGet('/revisions.ids', {page_id: pageId})
-    .then(res => {
+    this.props.crowi.apiGet('/revisions.ids', { page_id: pageId })
+      .then((res) => {
+
+        const rev = res.revisions;
+        const diffOpened = {};
+        const lastId = rev.length - 1;
+        res.revisions.forEach((revision, i) => {
+          const user = this.props.crowi.findUserById(revision.author);
+          if (user) {
+            rev[i].author = user;
+          }
+
+          if (i === 0 || i === lastId) {
+            diffOpened[revision._id] = true;
+          }
+          else {
+            diffOpened[revision._id] = false;
+          }
+        });
 
-      const rev = res.revisions;
-      let diffOpened = {};
-      const lastId = rev.length - 1;
-      res.revisions.map((revision, i) => {
-        const user = this.props.crowi.findUserById(revision.author);
-        if (user) {
-          rev[i].author = user;
-        }
+        this.setState({
+          revisions: rev,
+          diffOpened,
+        });
 
-        if (i === 0 || i === lastId) {
-          diffOpened[revision._id] = true;
+        // load 0, and last default
+        if (rev[0]) {
+          this.fetchPageRevisionBody(rev[0]);
         }
-        else {
-          diffOpened[revision._id] = false;
+        if (rev[1]) {
+          this.fetchPageRevisionBody(rev[1]);
         }
-      });
-
-      this.setState({
-        revisions: rev,
-        diffOpened: diffOpened,
-      });
-
-      // load 0, and last default
-      if (rev[0]) {
-        this.fetchPageRevisionBody(rev[0]);
-      }
-      if (rev[1]) {
-        this.fetchPageRevisionBody(rev[1]);
-      }
-      if (lastId !== 0 && lastId !== 1 && rev[lastId]) {
-        this.fetchPageRevisionBody(rev[lastId]);
-      }
-    }).catch(err => {
+        if (lastId !== 0 && lastId !== 1 && rev[lastId]) {
+          this.fetchPageRevisionBody(rev[lastId]);
+        }
+      })
+      .catch((err) => {
       // do nothing
-    });
+      });
   }
 
   getPreviousRevision(currentRevision) {
     let cursor = null;
-    for (let revision of this.state.revisions) {
+    for (const revision of this.state.revisions) {
+      // comparing ObjectId
+      // eslint-disable-next-line eqeqeq
       if (cursor && cursor._id == currentRevision._id) {
         cursor = revision;
         break;
@@ -80,12 +83,12 @@ class PageHistory extends React.Component {
   }
 
   onDiffOpenClicked(revision) {
-    const diffOpened = this.state.diffOpened,
-      revisionId = revision._id;
+    const diffOpened = this.state.diffOpened;
+    const revisionId = revision._id;
 
     diffOpened[revisionId] = !(diffOpened[revisionId]);
     this.setState({
-      diffOpened
+      diffOpened,
     });
 
     this.fetchPageRevisionBody(revision);
@@ -94,28 +97,29 @@ class PageHistory extends React.Component {
 
   fetchPageRevisionBody(revision) {
     if (revision.body) {
-      return ;
+      return;
     }
 
     this.props.crowi.apiGet('/revisions.get',
-      { page_id: this.props.pageId, revision_id: revision._id}
-    )
-    .then(res => {
-      if (res.ok) {
-        this.setState({
-          revisions: this.state.revisions.map((rev) => {
-            if (rev._id == res.revision._id) {
-              return res.revision;
-            }
-
-            return rev;
-          })
-        });
-      }
-    })
-    .catch(err => {
+      { page_id: this.props.pageId, revision_id: revision._id })
+      .then((res) => {
+        if (res.ok) {
+          this.setState({
+            revisions: this.state.revisions.map((rev) => {
+              // comparing ObjectId
+              // eslint-disable-next-line eqeqeq
+              if (rev._id == res.revision._id) {
+                return res.revision;
+              }
+
+              return rev;
+            }),
+          });
+        }
+      })
+      .catch((err) => {
 
-    });
+      });
   }
 
   render() {
@@ -131,10 +135,11 @@ class PageHistory extends React.Component {
       </div>
     );
   }
+
 }
 
 PageHistory.propTypes = {
-  t: PropTypes.func.isRequired,               // i18next
+  t: PropTypes.func.isRequired, // i18next
   pageId: PropTypes.string,
   crowi: PropTypes.object.isRequired,
 };

+ 8 - 5
src/client/js/components/PageHistory/RevisionDiff.js

@@ -7,9 +7,9 @@ import { Diff2Html } from 'diff2html';
 export default class RevisionDiff extends React.Component {
 
   render() {
-    const currentRevision = this.props.currentRevision,
-      previousRevision = this.props.previousRevision,
-      revisionDiffOpened = this.props.revisionDiffOpened;
+    const currentRevision = this.props.currentRevision;
+    const previousRevision = this.props.previousRevision;
+    const revisionDiffOpened = this.props.revisionDiffOpened;
 
 
     let diffViewHTML = '';
@@ -18,6 +18,8 @@ export default class RevisionDiff extends React.Component {
       && revisionDiffOpened) {
 
       let previousText = previousRevision.body;
+      // comparing ObjectId
+      // eslint-disable-next-line eqeqeq
       if (currentRevision._id == previousRevision._id) {
         previousText = '';
       }
@@ -25,15 +27,16 @@ export default class RevisionDiff extends React.Component {
       const patch = createPatch(
         currentRevision.path,
         previousText,
-        currentRevision.body
+        currentRevision.body,
       );
 
       diffViewHTML = Diff2Html.getPrettyHtml(patch);
     }
 
-    const diffView = {__html: diffViewHTML};
+    const diffView = { __html: diffViewHTML };
     return <div className="revision-history-diff" dangerouslySetInnerHTML={diffView} />;
   }
+
 }
 
 RevisionDiff.propTypes = {

+ 3 - 3
src/client/js/components/PageList/ListView.js

@@ -7,17 +7,18 @@ export default class ListView extends React.Component {
 
   render() {
     const listView = this.props.pages.map((page) => {
-      return <Page page={page} key={'page-list:list-view:' + page._id} />;
+      return <Page page={page} key={`page-list:list-view:${page._id}`} />;
     });
 
     return (
       <div className="page-list">
         <ul className="page-list-ul page-list-ul-flat">
-        {listView}
+          {listView}
         </ul>
       </div>
     );
   }
+
 }
 
 ListView.propTypes = {
@@ -25,5 +26,4 @@ ListView.propTypes = {
 };
 
 ListView.defaultProps = {
-  pages: [],
 };

+ 3 - 4
src/client/js/components/PageList/Page.js

@@ -15,7 +15,7 @@ export default class Page extends React.Component {
     }
 
     const styleFlex = {
-      flex: 1
+      flex: 1,
     };
 
     return (
@@ -26,20 +26,19 @@ export default class Page extends React.Component {
         </a>
         <PageListMeta page={page} />
         <div style={styleFlex}></div>
-        {this.props.children}
       </li>
     );
   }
+
 }
 
 Page.propTypes = {
   page: PropTypes.object.isRequired,
   linkTo: PropTypes.string,
+  excludePathString: PropTypes.string,
 };
 
 Page.defaultProps = {
-  page: {},
   linkTo: '',
   excludePathString: '',
 };
-

+ 2 - 3
src/client/js/components/PageList/PageListMeta.js

@@ -39,7 +39,7 @@ export default class PageListMeta extends React.Component {
     }
 
     let locked;
-    if (page.grant != 1) {
+    if (page.grant !== 1) {
       locked = <span><i className="icon-lock" /></span>;
     }
 
@@ -53,6 +53,7 @@ export default class PageListMeta extends React.Component {
       </span>
     );
   }
+
 }
 
 PageListMeta.propTypes = {
@@ -60,6 +61,4 @@ PageListMeta.propTypes = {
 };
 
 PageListMeta.defaultProps = {
-  page: {},
 };
-

+ 4 - 6
src/client/js/components/PageList/PagePath.js

@@ -6,7 +6,7 @@ import escapeStringRegexp from 'escape-string-regexp';
 export default class PagePath extends React.Component {
 
   getShortPath(path) {
-    let name = path.replace(/(\/)$/, '');
+    const name = path.replace(/(\/)$/, '');
 
     // /.../hoge/YYYY/MM/DD 形式のページ
     if (name.match(/.+\/([^/]+\/\d{4}\/\d{2}\/\d{2})$/)) {
@@ -34,7 +34,7 @@ export default class PagePath extends React.Component {
     const shortPath = this.getShortPath(pagePath);
 
     const shortPathEscaped = escapeStringRegexp(shortPath);
-    const pathPrefix = pagePath.replace(new RegExp(shortPathEscaped + '(/)?$'), '');
+    const pathPrefix = pagePath.replace(new RegExp(`${shortPathEscaped}(/)?$`), '');
 
     let classNames = ['page-path'];
     classNames = classNames.concat(this.props.additionalClassNames);
@@ -42,11 +42,10 @@ export default class PagePath extends React.Component {
     if (isShortPathOnly) {
       return <span className={classNames.join(' ')}>{shortPath}</span>;
     }
-    else {
-      return <span className={classNames.join(' ')}>{pathPrefix}<strong>{shortPath}</strong></span>;
-    }
 
+    return <span className={classNames.join(' ')}>{pathPrefix}<strong>{shortPath}</strong></span>;
   }
+
 }
 
 PagePath.propTypes = {
@@ -57,7 +56,6 @@ PagePath.propTypes = {
 };
 
 PagePath.defaultProps = {
-  page: {},
   additionalClassNames: [],
   excludePathString: '',
 };

+ 6 - 6
src/client/js/components/ReactUtils.js

@@ -14,14 +14,14 @@ export default class ReactUtils {
    * @memberOf ReactUtils
    */
   static nl2br(text) {
-    var regex = /(\n)/g;
-    return text.split(regex).map(function(line) {
+    const regex = /(\n)/g;
+    return text.split(regex).map((line) => {
       if (line.match(regex)) {
-        return React.createElement('br', {key: Math.random().toString(10).substr(2, 10)});
-      }
-      else {
-        return line;
+        return React.createElement('br', { key: Math.random().toString(10).substr(2, 10) });
       }
+
+      return line;
+
     });
   }
 

+ 5 - 4
src/client/js/components/SearchForm.js

@@ -29,7 +29,7 @@ export default class SearchForm extends React.Component {
   }
 
   onChange(selected) {
-    const page = selected[0];  // should be single page selected
+    const page = selected[0]; // should be single page selected
 
     // navigate to page
     if (page != null) {
@@ -43,7 +43,7 @@ export default class SearchForm extends React.Component {
     return (
       <table className="table m-1 search-help">
         <caption className="text-left text-primary p-2 mb-2">
-          <h5 className="m-1"><i className="icon-magnifier pr-2 mb-2"/>{ t('search_help.title') }</h5>
+          <h5 className="m-1"><i className="icon-magnifier pr-2 mb-2" />{ t('search_help.title') }</h5>
         </caption>
         <tbody>
           <tr>
@@ -55,7 +55,7 @@ export default class SearchForm extends React.Component {
           </tr>
           <tr>
             <th className="text-right pt-2">
-              <code>"This is GROWI"</code><br></br>
+              <code>&quot;This is GROWI&quot;</code><br></br>
               <small>({ t('search_help.phrase.syntax help') })</small>
             </th>
             <td><h6 className="m-0 pt-1">{ t('search_help.phrase.desc', { phrase: 'This is GROWI' }) }</h6></td>
@@ -97,10 +97,11 @@ export default class SearchForm extends React.Component {
       />
     );
   }
+
 }
 
 SearchForm.propTypes = {
-  t: PropTypes.func.isRequired,               // i18next
+  t: PropTypes.func.isRequired, // i18next
   crowi: PropTypes.object.isRequired,
   keyword: PropTypes.string,
   onSubmit: PropTypes.func.isRequired,

+ 31 - 32
src/client/js/components/SearchPage.js

@@ -13,12 +13,10 @@ class SearchPage extends React.Component {
     super(props);
 
     this.state = {
-      location: location,
       searchingKeyword: this.props.query.q || '',
       searchedKeyword: '',
       searchedPages: [],
       searchResultMeta: {},
-      searchError: null,
     };
 
     this.search = this.search.bind(this);
@@ -27,17 +25,17 @@ class SearchPage extends React.Component {
 
   componentDidMount() {
     const keyword = this.state.searchingKeyword;
-    if (keyword !== '')  {
-      this.search({keyword});
+    if (keyword !== '') {
+      this.search({ keyword });
     }
   }
 
   static getQueryByLocation(location) {
-    let search = location.search || '';
-    let query = {};
+    const search = location.search || '';
+    const query = {};
 
-    search.replace(/^\?/, '').split('&').forEach(function(element) {
-      let queryParts = element.split('=');
+    search.replace(/^\?/, '').split('&').forEach((element) => {
+      const queryParts = element.split('=');
       query[queryParts[0]] = decodeURIComponent(queryParts[1]).replace(/\+/g, ' ');
     });
 
@@ -45,7 +43,7 @@ class SearchPage extends React.Component {
   }
 
   changeURL(keyword, refreshHash) {
-    let hash = location.hash || '';
+    let hash = window.location.hash || '';
     // TODO 整理する
     if (refreshHash || this.state.searchedKeyword !== '') {
       hash = '';
@@ -62,7 +60,6 @@ class SearchPage extends React.Component {
         searchingKeyword: '',
         searchedPages: [],
         searchResultMeta: {},
-        searchError: null,
       });
 
       return true;
@@ -72,54 +69,56 @@ class SearchPage extends React.Component {
       searchingKeyword: keyword,
     });
 
-    this.props.crowi.apiGet('/search', {q: keyword})
-    .then(res => {
-      this.changeURL(keyword);
-
-      this.setState({
-        searchedKeyword: keyword,
-        searchedPages: res.data,
-        searchResultMeta: res.meta,
-      });
-    }).catch(err => {
-      // TODO error
-      this.setState({
-        searchError: err,
+    this.props.crowi.apiGet('/search', { q: keyword })
+      .then((res) => {
+        this.changeURL(keyword);
+
+        this.setState({
+          searchedKeyword: keyword,
+          searchedPages: res.data,
+          searchResultMeta: res.meta,
+        });
+      })
+      .catch((err) => {
+        // TODO error
+        // this.setState({
+        // });
       });
-    });
   }
 
   render() {
     return (
       <div>
         <div className="search-page-input">
-          <SearchPageForm t={this.props.t}
+          <SearchPageForm
+            t={this.props.t}
             crowi={this.props.crowi}
             onSearchFormChanged={this.search}
             keyword={this.state.searchingKeyword}
-            />
+          />
         </div>
         <SearchResult
-          crowi={this.props.crowi} crowiRenderer={this.props.crowiRenderer}
+          crowi={this.props.crowi}
+          crowiRenderer={this.props.crowiRenderer}
           pages={this.state.searchedPages}
           searchingKeyword={this.state.searchingKeyword}
           searchResultMeta={this.state.searchResultMeta}
-          />
+        />
       </div>
     );
   }
+
 }
 
 SearchPage.propTypes = {
-  t: PropTypes.func.isRequired,               // i18next
+  t: PropTypes.func.isRequired, // i18next
   crowi: PropTypes.object.isRequired,
   crowiRenderer: PropTypes.object.isRequired,
   query: PropTypes.object,
 };
 SearchPage.defaultProps = {
-  //pollInterval: 1000,
-  query: SearchPage.getQueryByLocation(location || {}),
-  searchError: null,
+  // pollInterval: 1000,
+  query: SearchPage.getQueryByLocation(window.location || {}),
 };
 
 export default translate()(SearchPage);

+ 4 - 8
src/client/js/components/SearchPage/DeletePageListModal.js

@@ -12,15 +12,11 @@ export default class DeletePageListModal extends React.Component {
    */
   static get OMIT_BODY_THRES() { return 400 }
 
-  constructor(props) {
-    super(props);
-  }
-
   componentWillMount() {
   }
 
   render() {
-    if (this.props.pages === undefined || this.props.pages.length == 0) {
+    if (this.props.pages == null || this.props.pages.length === 0) {
       return <div></div>;
     }
 
@@ -44,7 +40,7 @@ export default class DeletePageListModal extends React.Component {
           <div className="d-flex justify-content-between">
             <span className="text-danger">{this.props.errorMessage}</span>
             <span className="d-flex align-items-center">
-              <Checkbox className="text-danger" onClick={this.props.toggleDeleteCompletely} inline={true}>Delete completely</Checkbox>
+              <Checkbox className="text-danger" onClick={this.props.toggleDeleteCompletely} inline>Delete completely</Checkbox>
               <span className="m-l-10">
                 <Button onClick={this.props.confirmedToDelete}><i className="icon-trash"></i>Delete</Button>
               </span>
@@ -61,7 +57,7 @@ DeletePageListModal.propTypes = {
   isShown: PropTypes.bool.isRequired,
   pages: PropTypes.array,
   errorMessage: PropTypes.string,
-  cancel: PropTypes.func.isRequired,                 // for cancel evnet handling
-  confirmedToDelete: PropTypes.func.isRequired,      // for confirmed event handling
+  cancel: PropTypes.func.isRequired, //                 for cancel evnet handling
+  confirmedToDelete: PropTypes.func.isRequired, //      for confirmed event handling
   toggleDeleteCompletely: PropTypes.func.isRequired, // for delete completely check event handling
 };

+ 23 - 19
src/client/js/components/SearchPage/SearchPageForm.js

@@ -24,35 +24,39 @@ export default class SearchPageForm extends React.Component {
 
   search() {
     const keyword = this.state.keyword;
-    this.props.onSearchFormChanged({keyword: keyword});
-    this.setState({searchedKeyword: keyword});
+    this.props.onSearchFormChanged({ keyword });
+    this.setState({ searchedKeyword: keyword });
   }
 
   onInputChange(input) { // for only submitting with button
-    this.setState({keyword: input});
+    this.setState({ keyword: input });
   }
 
   render() {
-    return <FormGroup>
-      <InputGroup>
-        <SearchForm t={this.props.t}
-          crowi={this.props.crowi}
-          onSubmit={this.search}
-          keyword={this.state.searchedKeyword}
-          onInputChange={this.onInputChange}
-        />
-        <InputGroup.Button className="">
-          <Button onClick={this.search}>
-            <i className="icon-magnifier"></i>
-          </Button >
-        </InputGroup.Button>
-      </InputGroup>
-    </FormGroup>;
+    return (
+      <FormGroup>
+        <InputGroup>
+          <SearchForm
+            t={this.props.t}
+            crowi={this.props.crowi}
+            onSubmit={this.search}
+            keyword={this.state.searchedKeyword}
+            onInputChange={this.onInputChange}
+          />
+          <InputGroup.Button className="">
+            <Button onClick={this.search}>
+              <i className="icon-magnifier"></i>
+            </Button>
+          </InputGroup.Button>
+        </InputGroup>
+      </FormGroup>
+    );
   }
+
 }
 
 SearchPageForm.propTypes = {
-  t: PropTypes.func.isRequired,               // i18next
+  t: PropTypes.func.isRequired, // i18next
   crowi: PropTypes.object.isRequired,
   keyword: PropTypes.string,
   onSearchFormChanged: PropTypes.func.isRequired,

+ 88 - 70
src/client/js/components/SearchPage/SearchResult.js

@@ -1,3 +1,5 @@
+/* eslint-disable jsx-a11y/label-has-associated-control */
+/* eslint-disable jsx-a11y/label-has-for */
 import React from 'react';
 import PropTypes from 'prop-types';
 import * as toastr from 'toastr';
@@ -8,6 +10,7 @@ import DeletePageListModal from './DeletePageListModal';
 
 // Search.SearchResult
 export default class SearchResult extends React.Component {
+
   constructor(props) {
     super(props);
     this.state = {
@@ -50,8 +53,8 @@ export default class SearchResult extends React.Component {
     else {
       this.state.selectedPages.add(page);
     }
-    this.setState({isDeleteConfirmModalShown: false});
-    this.setState({selectedPages: this.state.selectedPages});
+    this.setState({ isDeleteConfirmModalShown: false });
+    this.setState({ selectedPages: this.state.selectedPages });
   }
 
   /**
@@ -61,7 +64,7 @@ export default class SearchResult extends React.Component {
    * @memberof SearchResult
    */
   isAllSelected() {
-    return this.state.selectedPages.size == this.props.pages.length;
+    return this.state.selectedPages.size === this.props.pages.length;
   }
 
   /**
@@ -77,10 +80,12 @@ export default class SearchResult extends React.Component {
       this.state.selectedPages.clear();
       this.props.pages.map((page) => {
         this.state.selectedPages.add(page);
+        return;
       });
     }
-    this.setState({selectedPages: this.state.selectedPages});
+    this.setState({ selectedPages: this.state.selectedPages });
   }
+
   /**
    * change deletion mode
    *
@@ -88,7 +93,7 @@ export default class SearchResult extends React.Component {
    */
   handleDeletionModeChange() {
     this.state.selectedPages.clear();
-    this.setState({deletionMode: !this.state.deletionMode});
+    this.setState({ deletionMode: !this.state.deletionMode });
   }
 
   /**
@@ -98,7 +103,7 @@ export default class SearchResult extends React.Component {
    */
   toggleDeleteCompletely() {
     // request で completely が undefined でないと指定アリと見なされるため
-    this.setState({isDeleteCompletely: this.state.isDeleteCompletely? undefined : true});
+    this.setState({ isDeleteCompletely: this.state.isDeleteCompletely ? undefined : true });
   }
 
   /**
@@ -107,41 +112,41 @@ export default class SearchResult extends React.Component {
    * @memberof SearchResult
    */
   deleteSelectedPages() {
-    let deleteCompletely = this.state.isDeleteCompletely;
+    const deleteCompletely = this.state.isDeleteCompletely;
     Promise.all(Array.from(this.state.selectedPages).map((page) => {
       return new Promise((resolve, reject) => {
         const pageId = page._id;
         const revisionId = page.revision._id;
-        this.props.crowi.apiPost('/pages.remove', {page_id: pageId, revision_id: revisionId, completely: deleteCompletely})
-          .then(res => {
+        this.props.crowi.apiPost('/pages.remove', { page_id: pageId, revision_id: revisionId, completely: deleteCompletely })
+          .then((res) => {
             if (res.ok) {
               this.state.selectedPages.delete(page);
               return resolve();
             }
-            else {
-              return reject();
-            }
+
+            return reject();
+
           })
-          .catch(err => {
-            console.log(err.message);   // eslint-disable-line no-console
-            this.setState({errorMessageForDeleting: err.message});
+          .catch((err) => {
+            console.log(err.message); // eslint-disable-line no-console
+            this.setState({ errorMessageForDeleting: err.message });
             return reject();
           });
       });
     }))
-    .then(() => {
-      window.location.reload();
-    })
-    .catch(err => {
-      toastr.error(err, 'Error occured', {
-        closeButton: true,
-        progressBar: true,
-        newestOnTop: false,
-        showDuration: '100',
-        hideDuration: '100',
-        timeOut: '3000',
+      .then(() => {
+        window.location.reload();
+      })
+      .catch((err) => {
+        toastr.error(err, 'Error occured', {
+          closeButton: true,
+          progressBar: true,
+          newestOnTop: false,
+          showDuration: '100',
+          hideDuration: '100',
+          timeOut: '3000',
+        });
       });
-    });
   }
 
   /**
@@ -150,7 +155,7 @@ export default class SearchResult extends React.Component {
    * @memberof SearchResult
    */
   showDeleteConfirmModal() {
-    this.setState({isDeleteConfirmModalShown: true});
+    this.setState({ isDeleteConfirmModalShown: true });
   }
 
   /**
@@ -168,8 +173,8 @@ export default class SearchResult extends React.Component {
   render() {
     const excludePathString = this.props.tree;
 
-    //console.log(this.props.searchError);
-    //console.log(this.isError());
+    // console.log(this.props.searchError);
+    // console.log(this.isError());
     if (this.isError()) {
       return (
         <div className="content-main">
@@ -189,7 +194,7 @@ export default class SearchResult extends React.Component {
       }
       return (
         <div className="content-main">
-            <i className="icon-fw icon-info" /> No page found with &quot;{this.props.searchingKeyword}&quot;{under}
+          <i className="icon-fw icon-info" /> No page found with &quot;{this.props.searchingKeyword}&quot;{under}
         </div>
       );
 
@@ -199,48 +204,63 @@ export default class SearchResult extends React.Component {
     let allSelectCheck = '';
 
     if (this.state.deletionMode) {
-      deletionModeButtons =
-      <div className="btn-group">
-        <button type="button" className="btn btn-rounded btn-default btn-xs" onClick={() => this.handleDeletionModeChange()}>
-          <i className="icon-ban"/> Cancel
-        </button>
-        <button type="button" className="btn btn-rounded btn-danger btn-xs" onClick={() => this.showDeleteConfirmModal()} disabled={this.state.selectedPages.size == 0}>
-          <i className="icon-trash"/> Delete
-        </button>
-      </div>;
-      allSelectCheck =
-      <div>
-        <label>
-          <input
-            type="checkbox"
-            onClick={() => this.handleAllSelect()}
-            checked={this.isAllSelected()} />
+      deletionModeButtons = (
+        <div className="btn-group">
+          <button type="button" className="btn btn-rounded btn-default btn-xs" onClick={() => { return this.handleDeletionModeChange() }}>
+            <i className="icon-ban" /> Cancel
+          </button>
+          <button
+            type="button"
+            className="btn btn-rounded btn-danger btn-xs"
+            onClick={() => { return this.showDeleteConfirmModal() }}
+            disabled={this.state.selectedPages.size === 0}
+          >
+            <i className="icon-trash" /> Delete
+          </button>
+        </div>
+      );
+      allSelectCheck = (
+        <div>
+          <label>
+            <input
+              type="checkbox"
+              onClick={() => { return this.handleAllSelect() }}
+              checked={this.isAllSelected()}
+            />
             &nbsp;Check All
-        </label>
-      </div>;
+          </label>
+        </div>
+      );
     }
     else {
-      deletionModeButtons =
-      <div className="btn-group">
-        <button type="button" className="btn btn-default btn-rounded btn-xs" onClick={() => this.handleDeletionModeChange()}>
-          <i className="ti-check-box"/> DeletionMode
-        </button>
-      </div>;
+      deletionModeButtons = (
+        <div className="btn-group">
+          <button type="button" className="btn btn-default btn-rounded btn-xs" onClick={() => { return this.handleDeletionModeChange() }}>
+            <i className="ti-check-box" /> DeletionMode
+          </button>
+        </div>
+      );
     }
 
     const listView = this.props.pages.map((page) => {
-      const pageId = '#' + page._id;
+      const pageId = `#${page._id}`;
       return (
-        <Page page={page}
+        <Page
+          page={page}
           linkTo={pageId}
           key={page._id}
           excludePathString={excludePathString}
-          >
-          { this.state.deletionMode &&
-            <input type="checkbox" className="search-result-list-delete-checkbox"
+        >
+          { this.state.deletionMode
+            && (
+            <input
+              type="checkbox"
+              className="search-result-list-delete-checkbox"
               value={pageId}
               checked={this.state.selectedPages.has(page)}
-              onClick={() => this.toggleCheckbox(page)} />
+              onClick={() => { return this.toggleCheckbox(page) }}
+            />
+)
             }
           <div className="page-list-option">
             <a href={page.path}><i className="icon-login" /></a>
@@ -251,7 +271,7 @@ export default class SearchResult extends React.Component {
 
     // TODO あとでなんとかする
     setTimeout(() => {
-      $('#search-result-list > nav').affix({ offset: { top: 50 }});
+      $('#search-result-list > nav').affix({ offset: { top: 50 } });
     }, 1200);
 
     /*
@@ -280,10 +300,11 @@ export default class SearchResult extends React.Component {
           </div>
           <div className="col-md-8 search-result-content" id="search-result-content">
             <SearchResultList
-              crowi={this.props.crowi} crowiRenderer={this.props.crowiRenderer}
+              crowi={this.props.crowi}
+              crowiRenderer={this.props.crowiRenderer}
               pages={this.props.pages}
               searchingKeyword={this.props.searchingKeyword}
-              />
+            />
           </div>
         </div>
         <DeletePageListModal
@@ -295,9 +316,10 @@ export default class SearchResult extends React.Component {
           toggleDeleteCompletely={this.toggleDeleteCompletely}
         />
 
-      </div>//content-main
+      </div>// content-main
     );
   }
+
 }
 
 SearchResult.propTypes = {
@@ -307,12 +329,8 @@ SearchResult.propTypes = {
   pages: PropTypes.array.isRequired,
   searchingKeyword: PropTypes.string.isRequired,
   searchResultMeta: PropTypes.object.isRequired,
-  searchError: PropTypes.object
+  searchError: PropTypes.object,
 };
 SearchResult.defaultProps = {
-  tree: '',
-  pages: [],
-  searchingKeyword: '',
-  searchResultMeta: {},
   searchError: null,
 };

+ 3 - 4
src/client/js/components/SearchPage/SearchResultList.js

@@ -10,7 +10,7 @@ export default class SearchResultList extends React.Component {
   constructor(props) {
     super(props);
 
-    this.growiRenderer = new GrowiRenderer(this.props.crowi, this.props.crowiRenderer, {mode: 'searchresult'});
+    this.growiRenderer = new GrowiRenderer(this.props.crowi, this.props.crowiRenderer, { mode: 'searchresult' });
   }
 
   render() {
@@ -32,10 +32,11 @@ export default class SearchResultList extends React.Component {
 
     return (
       <div>
-      {resultList}
+        {resultList}
       </div>
     );
   }
+
 }
 
 SearchResultList.propTypes = {
@@ -46,6 +47,4 @@ SearchResultList.propTypes = {
 };
 
 SearchResultList.defaultProps = {
-  pages: [],
-  searchingKeyword: '',
 };

+ 25 - 27
src/client/js/components/SearchTypeahead.js

@@ -16,8 +16,6 @@ export default class SearchTypeahead extends React.Component {
 
     this.state = {
       input: this.props.keywordOnInit,
-      keyword: '',
-      searchedKeyword: '',
       pages: [],
       isLoading: false,
       searchError: null,
@@ -39,7 +37,7 @@ export default class SearchTypeahead extends React.Component {
    * Get instance of AsyncTypeahead
    */
   getTypeahead() {
-    return this.refs.typeahead ? this.refs.typeahead.getInstance() : null;
+    return this.typeahead ? this.typeahead.getInstance() : null;
   }
 
   componentDidMount() {
@@ -54,7 +52,7 @@ export default class SearchTypeahead extends React.Component {
   restoreInitialData() {
     // see https://github.com/ericgio/react-bootstrap-typeahead/issues/266#issuecomment-414987723
     const text = this.props.keywordOnInit;
-    const instance = this.refs.typeahead.getInstance();
+    const instance = this.typeahead.getInstance();
     instance.clear();
     instance.setState({ text });
   }
@@ -62,18 +60,14 @@ export default class SearchTypeahead extends React.Component {
   search(keyword) {
 
     if (keyword === '') {
-      this.setState({
-        keyword: '',
-        searchedKeyword: '',
-      });
       return;
     }
 
-    this.setState({isLoading: true});
+    this.setState({ isLoading: true });
 
-    this.crowi.apiGet('/search', {q: keyword})
-      .then(res => { this.onSearchSuccess(res) })
-      .catch(err => { this.onSearchError(err) });
+    this.crowi.apiGet('/search', { q: keyword })
+      .then((res) => { this.onSearchSuccess(res) })
+      .catch((err) => { this.onSearchError(err) });
   }
 
   /**
@@ -83,10 +77,11 @@ export default class SearchTypeahead extends React.Component {
   onSearchSuccess(res) {
     this.setState({
       isLoading: false,
-      keyword: '',
       pages: res.data,
     });
-    this.props.onSearchSuccess && this.props.onSearchSuccess(res);
+    if (this.props.onSearchSuccess != null) {
+      this.props.onSearchSuccess(res);
+    }
   }
 
   /**
@@ -98,14 +93,16 @@ export default class SearchTypeahead extends React.Component {
       isLoading: false,
       searchError: err,
     });
-    this.props.onSearchError && this.props.onSearchError(err);
+    if (this.props.onSearchError != null) {
+      this.props.onSearchError(err);
+    }
   }
 
   onInputChange(text) {
-    this.setState({input: text});
+    this.setState({ input: text });
     this.props.onInputChange(text);
     if (text === '') {
-      this.setState({pages: []});
+      this.setState({ pages: [] });
     }
   }
 
@@ -141,9 +138,9 @@ export default class SearchTypeahead extends React.Component {
    * Get restore form button to initialize button
    */
   getRestoreFormButton() {
-    let isHidden = (this.state.input === this.props.keywordOnInit);
+    const isHidden = (this.state.input === this.props.keywordOnInit);
 
-    return isHidden ? <span></span> : (
+    return isHidden ? <span /> : (
       <button type="button" className="btn btn-link search-clear" onMouseDown={this.restoreInitialData}>
         <i className="icon-close" />
       </button>
@@ -154,16 +151,16 @@ export default class SearchTypeahead extends React.Component {
     const page = option;
     return (
       <span>
-      <UserPicture user={page.lastUpdateUser} size="sm" />
-      <PagePath page={page} />
-      <PageListMeta page={page} />
+        <UserPicture user={page.lastUpdateUser} size="sm" />
+        <PagePath page={page} />
+        <PageListMeta page={page} />
       </span>
     );
   }
 
   render() {
-    const defaultSelected = (this.props.keywordOnInit != '')
-      ? [{path: this.props.keywordOnInit}]
+    const defaultSelected = (this.props.keywordOnInit !== '')
+      ? [{ path: this.props.keywordOnInit }]
       : [];
     const inputProps = { autoComplete: 'off' };
     if (this.props.inputName != null) {
@@ -176,7 +173,7 @@ export default class SearchTypeahead extends React.Component {
       <div className="search-typeahead">
         <AsyncTypeahead
           {...this.props}
-          ref="typeahead"
+          ref={(c) => { this.typeahead = c }}
           inputProps={inputProps}
           isLoading={this.state.isLoading}
           labelKey="path"
@@ -187,8 +184,8 @@ export default class SearchTypeahead extends React.Component {
               // DIRTY HACK
               //  note: The default searchText string has been shown wrongly even if isLoading is false
               //        since upgrade react-bootstrap-typeahead to v3.3.2 -- 2019.02.05 Yuki Takei
-          align='left'
-          submitFormOnEnter={true}
+          align="left"
+          submitFormOnEnter
           onSearch={this.search}
           onInputChange={this.onInputChange}
           onKeyDown={this.onKeyDown}
@@ -201,6 +198,7 @@ export default class SearchTypeahead extends React.Component {
       </div>
     );
   }
+
 }
 
 /**

+ 6 - 7
src/client/js/components/User/User.js

@@ -7,7 +7,7 @@ export default class User extends React.Component {
 
   render() {
     const user = this.props.user;
-    const userLink = '/user/' + user.username;
+    const userLink = `/user/${user.username}`;
 
     const username = this.props.username;
     const name = this.props.name;
@@ -17,16 +17,17 @@ export default class User extends React.Component {
         <a href={userLink}>
           <UserPicture user={user} />
 
-          {username &&
-              <span className="user-component-username">@{user.username}</span>
+          {username
+              && <span className="user-component-username">@{user.username}</span>
           }
-          {name &&
-              <span className="user-component-name">({user.name})</span>
+          {name
+              && <span className="user-component-name">({user.name})</span>
           }
         </a>
       </span>
     );
   }
+
 }
 
 User.propTypes = {
@@ -36,6 +37,4 @@ User.propTypes = {
 };
 
 User.defaultProps = {
-  name: false,
-  username: false,
 };

+ 9 - 9
src/client/js/components/User/UserPicture.js

@@ -11,15 +11,15 @@ export default class UserPicture extends React.Component {
       return this.generateGravatarSrc(user);
     }
     // uploaded image
-    else if (user.image != null) {
+    if (user.image != null) {
       return user.image;
     }
-    else if (user.imageAttachment != null) {
+    if (user.imageAttachment != null) {
       return user.imageAttachment.filePathProxied;
     }
-    else {
-      return '/images/icons/user.svg';
-    }
+
+    return '/images/icons/user.svg';
+
   }
 
   generateGravatarSrc(user) {
@@ -29,10 +29,10 @@ export default class UserPicture extends React.Component {
   }
 
   getClassName() {
-    let className = ['img-circle', 'picture'];
+    const className = ['img-circle', 'picture'];
     // size
     if (this.props.size) {
-      className.push('picture-' + this.props.size);
+      className.push(`picture-${this.props.size}`);
     }
 
     return className.join(' ');
@@ -46,9 +46,10 @@ export default class UserPicture extends React.Component {
         src={this.getUserPicture(user)}
         alt={user.username}
         className={this.getClassName()}
-        />
+      />
     );
   }
+
 }
 
 UserPicture.propTypes = {
@@ -57,6 +58,5 @@ UserPicture.propTypes = {
 };
 
 UserPicture.defaultProps = {
-  user: {},
   size: null,
 };

+ 20 - 19
src/client/js/hackmd-agent.js

@@ -16,7 +16,7 @@ import { debounce } from 'throttle-debounce';
 
 /* eslint-disable no-console  */
 
-const allowedOrigin = '{{origin}}';         // will be replaced by swig
+const allowedOrigin = '{{origin}}'; // will be replaced by swig
 
 
 /**
@@ -47,14 +47,14 @@ function setValueToCodemirrorOnInit(newValue) {
     setValueToCodemirror(newValue);
     return;
   }
-  else {
-    const intervalId = setInterval(() => {
-      if (window.cmClient != null) {
-        clearInterval(intervalId);
-        setValueToCodemirror(newValue);
-      }
-    }, 250);
-  }
+
+  const intervalId = setInterval(() => {
+    if (window.cmClient != null) {
+      clearInterval(intervalId);
+      setValueToCodemirror(newValue);
+    }
+  }, 250);
+
 }
 
 /**
@@ -86,7 +86,7 @@ function addEventListenersToCodemirror() {
     return;
   }
 
-  //// change event
+  // == change event
   editor.on('change', (cm, change) => {
     if (change.origin === 'ignoreHistory') {
       // do nothing because this operation triggered by other user
@@ -95,7 +95,7 @@ function addEventListenersToCodemirror() {
     debouncedPostParentToNotifyBodyChanges(cm.doc.getValue());
   });
 
-  //// save event
+  // == save event
   // Reset save commands and Cmd-S/Ctrl-S shortcuts that initialized by HackMD
   codemirror.commands.save = function(cm) {
     postParentToSaveWithShortcut(cm.doc.getValue());
@@ -117,14 +117,16 @@ function connectToParentWithPenpal() {
       },
       setValueOnInit(newValue) {
         setValueToCodemirrorOnInit(newValue);
-      }
-    }
-  });
-  connection.promise.then(parent => {
-    window.growi = parent;
-  }).catch(err => {
-    console.log(err);
+      },
+    },
   });
+  connection.promise
+    .then((parent) => {
+      window.growi = parent;
+    })
+    .catch((err) => {
+      console.log(err);
+    });
 }
 
 /**
@@ -148,4 +150,3 @@ function connectToParentWithPenpal() {
 
   console.log('[HackMD] GROWI agent for HackMD has successfully loaded.');
 }());
-

+ 1 - 1
src/client/js/hackmd-styles.js

@@ -12,7 +12,7 @@
 
 /* eslint-disable no-console  */
 
-const styles = '{{styles}}';         // will be replaced by swig
+const styles = '{{styles}}'; // will be replaced by swig
 
 /**
  * Insert link tag to load style file

+ 2 - 2
src/client/js/i18n.js

@@ -39,7 +39,7 @@ export default (userlang) => {
         withRef: true,
         bindI18n: 'languageChanged loaded',
         bindStore: 'added removed',
-        nsMode: 'default'
-      }
+        nsMode: 'default',
+      },
     });
 };

+ 1 - 1
src/client/js/installer.js

@@ -19,6 +19,6 @@ if (installerFormElem) {
     <I18nextProvider i18n={i18n}>
       <InstallerForm userName={userName} name={name} email={email} csrf={csrf} />
     </I18nextProvider>,
-    installerFormElem
+    installerFormElem,
   );
 }

+ 19 - 17
src/client/js/legacy/crowi-admin.js

@@ -1,3 +1,6 @@
+/* global Switchery */
+/* eslint-disable no-restricted-globals */
+
 require('./thirdparty-js/jQuery.style.switcher');
 
 // see https://github.com/abpetkov/switchery/issues/120
@@ -5,9 +8,9 @@ require('./thirdparty-js/jQuery.style.switcher');
 require('./thirdparty-js/switchery/switchery');
 require('./thirdparty-js/switchery/switchery.css');
 
-$(function() {
+$(() => {
   $('#slackNotificationForm').on('submit', function(e) {
-    $.post('/_api/admin/notification.add', $(this).serialize(), function(res) {
+    $.post('/_api/admin/notification.add', $(this).serialize(), (res) => {
       if (res.ok) {
         // TODO Fix
         location.reload();
@@ -18,7 +21,7 @@ $(function() {
   });
 
   $('form.admin-remove-updatepost').on('submit', function(e) {
-    $.post('/_api/admin/notification.remove', $(this).serialize(), function(res) {
+    $.post('/_api/admin/notification.remove', $(this).serialize(), (res) => {
       if (res.ok) {
         // TODO Fix
         location.reload();
@@ -29,7 +32,7 @@ $(function() {
 
   $('#createdUserModal').modal('show');
 
-  $('#admin-password-reset-modal').on('show.bs.modal', function(button) {
+  $('#admin-password-reset-modal').on('show.bs.modal', (button) => {
     const data = $(button.relatedTarget);
     const userId = data.data('user-id');
     const email = data.data('user-email');
@@ -39,16 +42,16 @@ $(function() {
   });
 
   $('form#admin-users-reset-password').on('submit', function(e) {
-    $.post('/_api/admin/users.resetPassword', $(this).serialize(), function(res) {
+    $.post('/_api/admin/users.resetPassword', $(this).serialize(), (res) => {
       if (res.ok) {
         // TODO Fix
-        //location.reload();
+        // location.reload();
         $('#admin-password-reset-modal').modal('hide');
         $('#admin-password-reset-modal-done').modal('show');
 
         $('#admin-password-reset-done-user').text(res.user.email);
         $('#admin-password-reset-done-password').text(res.newPassword);
-        return ;
+        return;
       }
 
       // fixme
@@ -58,7 +61,7 @@ $(function() {
     return false;
   });
 
-  $('#admin-delete-user-group-modal').on('show.bs.modal', function(button) {
+  $('#admin-delete-user-group-modal').on('show.bs.modal', (button) => {
     const data = $(button.relatedTarget);
     const userGroupId = data.data('user-group-id');
     const userGroupName = data.data('user-group-name');
@@ -68,7 +71,7 @@ $(function() {
   });
 
   $('form#user-group-relation-create').on('submit', function(e) {
-    $.post('/admin/user-group-relation/create', $(this).serialize(), function(res) {
+    $.post('/admin/user-group-relation/create', $(this).serialize(), (res) => {
       $('#admin-add-user-group-relation-modal').modal('hide');
       return;
     });
@@ -78,7 +81,7 @@ $(function() {
   $('#pictureUploadForm input[name=userGroupPicture]').on('change', function() {
     const $form = $('#pictureUploadForm');
     const fd = new FormData($form[0]);
-    if ($(this).val() == '') {
+    if ($(this).val() !== '') {
       return false;
     }
 
@@ -89,9 +92,9 @@ $(function() {
       contentType: false,
       data: fd,
       dataType: 'json',
-      success: function(data) {
+      success(data) {
         if (data.status) {
-          $('#settingUserPicture').attr('src', data.url + '?time=' + (new Date()));
+          $('#settingUserPicture').attr('src', `${data.url}?time=${new Date()}`);
           $('#pictureUploadFormMessage')
             .addClass('alert alert-success')
             .html('変更しました');
@@ -102,7 +105,7 @@ $(function() {
             .html('変更中にエラーが発生しました。');
         }
         $('#pictureUploadFormProgress').html('');
-      }
+      },
     });
     return false;
   });
@@ -112,11 +115,10 @@ $(function() {
 
   // switchery
   const elems = Array.prototype.slice.call(document.querySelectorAll('.js-switch'));
-  elems.forEach(function(elem) {
+  elems.forEach((elem) => {
     const color = elem.dataset.color;
     const size = elem.dataset.size;
-    new Switchery(elem, { color, size });   // eslint-disable-line no-undef
+    // eslint-disable-next-line no-new
+    new Switchery(elem, { color, size });
   });
 });
-
-

+ 4 - 4
src/client/js/legacy/crowi-presentation.js

@@ -15,8 +15,8 @@ Reveal.initialize({
   center: true,
   transition: 'slide',
 
-  //// This specification method can't be used
-  ////   sice deleting symlink prevented `src` from being resolved -- 2017.06.15 Yuki Takei
+  // // This specification method can't be used
+  // //   sice deleting symlink prevented `src` from being resolved -- 2017.06.15 Yuki Takei
   //
   // Optional libraries used to extend on reveal.js
   // dependencies: [
@@ -40,11 +40,11 @@ require.ensure([], () => {
   Reveal.sync();
 });
 
-Reveal.addEventListener('ready', function(event) {
+Reveal.addEventListener('ready', (event) => {
   // event.currentSlide, event.indexh, event.indexv
   $('.reveal section').each(function(e) {
     const $self = $(this);
-    if ($self.children().length == 1) {
+    if ($self.children().length !== 1) {
       $self.addClass('only');
     }
   });

+ 136 - 135
src/client/js/legacy/crowi.js

@@ -1,24 +1,23 @@
-/* jshint browser: true, jquery: true */
-/* Author: Sotaro KARASAWA <sotarok@crocos.co.jp>
-*/
+/* global location */
+/* eslint no-restricted-globals: ['error', 'locaion'] */
 
 import React from 'react';
 import ReactDOM from 'react-dom';
 
 import { debounce } from 'throttle-debounce';
 
+import * as pathUtils from '@commons/util/path-utils';
+import GrowiRenderer from '../util/GrowiRenderer';
+import RevisionLoader from '../components/Page/RevisionLoader';
+
 const entities = require('entities');
 const escapeStringRegexp = require('escape-string-regexp');
 require('jquery.cookie');
 require('bootstrap-select');
 
-import * as pathUtils from '@commons/util/path-utils';
-import GrowiRenderer from '../util/GrowiRenderer';
-import RevisionLoader from '../components/Page/RevisionLoader';
-
 require('./thirdparty-js/agile-admin');
 
-let Crowi = {};
+const Crowi = {};
 
 if (!window) {
   window = {};
@@ -97,7 +96,7 @@ Crowi.modifyScrollTop = function() {
   if (window.scrollY === 0) {
     timeout = 200;
   }
-  setTimeout(function() {
+  setTimeout(() => {
     const sectionHeaderRect = sectionHeader.getBoundingClientRect();
     if (sectionHeaderRect.top >= pageHeaderRect.bottom) {
       return;
@@ -140,10 +139,10 @@ Crowi.initAffix = () => {
     const containerHeight = $affixContentContainer.outerHeight(true);
     $affixContent.affix({
       offset: {
-        top: function() {
+        top() {
           return $('.navbar').outerHeight(true) + containerHeight;
-        }
-      }
+        },
+      },
     });
     $('[data-affix-disable]').on('click', function(e) {
       const $elm = $($(this).data('affix-disable'));
@@ -151,7 +150,7 @@ Crowi.initAffix = () => {
       $elm.removeData('affix').removeClass('affix affix-top affix-bottom');
       return false;
     });
-    $affixContentContainer.css({'min-height': containerHeight});
+    $affixContentContainer.css({ 'min-height': containerHeight });
   }
 };
 
@@ -173,7 +172,7 @@ Crowi.initSlimScrollForRevisionToc = () => {
     // window height - revisionTocTop - .system-version height
     let h = window.innerHeight - revisionTocTop - 20;
 
-    const tocContentHeight = tocContentElem.getBoundingClientRect().height + 15;  // add margin
+    const tocContentHeight = tocContentElem.getBoundingClientRect().height + 15; // add margin
 
     h = Math.min(h, tocContentHeight);
 
@@ -198,11 +197,11 @@ Crowi.initSlimScrollForRevisionToc = () => {
     resetScrollbarDebounced(getCurrentRevisionTocTop());
   });
   // affix on
-  $('#revision-toc').on('affixed.bs.affix', function() {
+  $('#revision-toc').on('affixed.bs.affix', () => {
     resetScrollbar(getCurrentRevisionTocTop());
   });
   // affix off
-  $('#revision-toc').on('affixed-top.bs.affix', function() {
+  $('#revision-toc').on('affixed-top.bs.affix', () => {
     // calculate sum of height (.navbar-header + .bg-title) + margin-top of .main
     const sum = 138;
     resetScrollbar(sum);
@@ -215,13 +214,13 @@ Crowi.findHashFromUrl = function(url) {
   if (match = url.match(/#(.+)$/)) {
     return `#${match[1]}`;
   }
-  /* eslint-enable */
+  /* eslint-enable no-cond-assign */
 
   return '';
 };
 
 Crowi.findSectionHeader = function(hash) {
-  if (hash.length == 0) {
+  if (hash.length === 0) {
     return;
   }
 
@@ -230,7 +229,7 @@ Crowi.findSectionHeader = function(hash) {
   // don't use jQuery and document.querySelector
   //  because hash may containe Base64 encoded strings
   const elem = document.getElementById(id);
-  if (elem != null && elem.tagName.match(/h\d+/i)) {  // match h1, h2, h3...
+  if (elem != null && elem.tagName.match(/h\d+/i)) { // match h1, h2, h3...
     return elem;
   }
 
@@ -264,12 +263,11 @@ Crowi.getCurrentEditorMode = function() {
   if ($('body').hasClass('builtin-editor')) {
     return 'builtin';
   }
-  else {
-    return 'hackmd';
-  }
+
+  return 'hackmd';
 };
 
-$(function() {
+$(() => {
   const crowi = window.crowi;
   const config = JSON.parse(document.getElementById('crowi-context-hydrate').textContent || '{}');
 
@@ -278,14 +276,14 @@ $(function() {
   // const revisionCreatedAt = $('#content-main').data('page-revision-created');
   // const currentUser = $('#content-main').data('current-user');
   const isSeen = $('#content-main').data('page-is-seen');
-  const pagePath= $('#content-main').data('path');
-  const isSavedStatesOfTabChanges = config['isSavedStatesOfTabChanges'];
+  const pagePath = $('#content-main').data('path');
+  const isSavedStatesOfTabChanges = config.isSavedStatesOfTabChanges;
 
   $('[data-toggle="popover"]').popover();
   $('[data-toggle="tooltip"]').tooltip();
   $('[data-tooltip-stay]').tooltip('show');
 
-  $('#toggle-sidebar').click(function(e) {
+  $('#toggle-sidebar').click((e) => {
     const $mainContainer = $('.main-container');
     if ($mainContainer.hasClass('aside-hidden')) {
       $('.main-container').removeClass('aside-hidden');
@@ -298,7 +296,7 @@ $(function() {
     return false;
   });
 
-  if ($.cookie('aside-hidden') == 1) {
+  if ($.cookie('aside-hidden') === 1) {
     $('.main-container').addClass('aside-hidden');
   }
 
@@ -307,14 +305,14 @@ $(function() {
   });
 
 
-  $('#create-page').on('shown.bs.modal', function(e) {
+  $('#create-page').on('shown.bs.modal', (e) => {
     // quick hack: replace from server side rendering "date" to client side "date"
     const today = new Date();
-    const month = ('0' + (today.getMonth() + 1)).slice(-2);
-    const day = ('0' + today.getDate()).slice(-2);
-    const dateString = today.getFullYear() + '/' + month + '/' + day;
-    $('#create-page-today .page-today-suffix').text('/' + dateString + '/');
-    $('#create-page-today .page-today-input2').data('prefix', '/' + dateString + '/');
+    const month = (`0${today.getMonth() + 1}`).slice(-2);
+    const day = (`0${today.getDate()}`).slice(-2);
+    const dateString = `${today.getFullYear()}/${month}/${day}`;
+    $('#create-page-today .page-today-suffix').text(`/${dateString}/`);
+    $('#create-page-today .page-today-input2').data('prefix', `/${dateString}/`);
 
     // focus
     $('#create-page-today .page-today-input2').eq(0).focus();
@@ -331,69 +329,69 @@ $(function() {
     if (input2 === '') {
       prefix2 = prefix2.slice(0, -1);
     }
-    top.location.href = prefix1 + input1 + prefix2 + input2 + '#edit';
+    top.location.href = `${prefix1 + input1 + prefix2 + input2}#edit`;
     return false;
   });
 
   $('#create-page-under-tree').submit(function(e) {
     let name = $('input', this).val();
     if (!name.match(/^\//)) {
-      name = '/' + name;
+      name = `/${name}`;
     }
     if (name.match(/.+\/$/)) {
       name = name.substr(0, name.length - 1);
     }
-    top.location.href = pathUtils.encodePagePath(name) + '#edit';
+    top.location.href = `${pathUtils.encodePagePath(name)}#edit`;
     return false;
   });
 
   // rename/unportalize
-  $('#renamePage, #unportalize').on('shown.bs.modal', function(e) {
+  $('#renamePage, #unportalize').on('shown.bs.modal', (e) => {
     $('#renamePage #newPageName').focus();
     $('#renamePage .msg, #unportalize .msg').hide();
   });
   $('#renamePageForm, #unportalize-form').submit(function(e) {
     // create name-value map
-    let nameValueMap = {};
+    const nameValueMap = {};
     $(this).serializeArray().forEach((obj) => {
       nameValueMap[obj.name] = obj.value; // nameValueMap.new_path is renamed page path
     });
 
-    const data = $(this).serialize() + `&socketClientId=${crowi.getSocketClientId()}`;
+    const data = `${$(this).serialize()}&socketClientId=${crowi.getSocketClientId()}`;
 
     $.ajax({
       type: 'POST',
       url: '/_api/pages.rename',
-      data: data,
-      dataType: 'json'
+      data,
+      dataType: 'json',
     })
-    .done(function(res) {
+      .done((res) => {
       // error
-      if (!res.ok) {
-        const linkPath = pathUtils.normalizePath(nameValueMap.new_path);
-        $('#renamePage .msg, #unportalize .msg').hide();
-        $(`#renamePage .msg-${res.code}, #unportalize .msg-${res.code}`).show();
-        $('#renamePage #linkToNewPage, #unportalize #linkToNewPage').html(`
+        if (!res.ok) {
+          const linkPath = pathUtils.normalizePath(nameValueMap.new_path);
+          $('#renamePage .msg, #unportalize .msg').hide();
+          $(`#renamePage .msg-${res.code}, #unportalize .msg-${res.code}`).show();
+          $('#renamePage #linkToNewPage, #unportalize #linkToNewPage').html(`
           <a href="${linkPath}">${linkPath} <i class="icon-login"></i></a>
         `);
-      }
-      else {
-        const page = res.page;
-        top.location.href = page.path + '?renamed=' + pagePath;
-      }
-    });
+        }
+        else {
+          const page = res.page;
+          top.location.href = `${page.path}?renamed=${pagePath}`;
+        }
+      });
 
     return false;
   });
 
   // duplicate
-  $('#duplicatePage').on('shown.bs.modal', function(e) {
+  $('#duplicatePage').on('shown.bs.modal', (e) => {
     $('#duplicatePage #duplicatePageName').focus();
     $('#duplicatePage .msg').hide();
   });
   $('#duplicatePageForm, #unportalize-form').submit(function(e) {
     // create name-value map
-    let nameValueMap = {};
+    const nameValueMap = {};
     $(this).serializeArray().forEach((obj) => {
       nameValueMap[obj.name] = obj.value; // nameValueMap.new_path is duplicated page path
     });
@@ -402,8 +400,8 @@ $(function() {
       type: 'POST',
       url: '/_api/pages.duplicate',
       data: $(this).serialize(),
-      dataType: 'json'
-    }).done(function(res) {
+      dataType: 'json',
+    }).done((res) => {
       // error
       if (!res.ok) {
         const linkPath = pathUtils.normalizePath(nameValueMap.new_path);
@@ -415,7 +413,7 @@ $(function() {
       }
       else {
         const page = res.page;
-        top.location.href = page.path + '?duplicated=' + pagePath;
+        top.location.href = `${page.path}?duplicated=${pagePath}`;
       }
     });
 
@@ -423,16 +421,16 @@ $(function() {
   });
 
   // delete
-  $('#deletePage').on('shown.bs.modal', function(e) {
+  $('#deletePage').on('shown.bs.modal', (e) => {
     $('#deletePage .msg').hide();
   });
-  $('#delete-page-form').submit(function(e) {
+  $('#delete-page-form').submit((e) => {
     $.ajax({
       type: 'POST',
       url: '/_api/pages.remove',
       data: $('#delete-page-form').serialize(),
-      dataType: 'json'
-    }).done(function(res) {
+      dataType: 'json',
+    }).done((res) => {
       // error
       if (!res.ok) {
         $('#deletePage .msg').hide();
@@ -448,16 +446,16 @@ $(function() {
   });
 
   // Put Back
-  $('#putBackPage').on('shown.bs.modal', function(e) {
+  $('#putBackPage').on('shown.bs.modal', (e) => {
     $('#putBackPage .msg').hide();
   });
-  $('#revert-delete-page-form').submit(function(e) {
+  $('#revert-delete-page-form').submit((e) => {
     $.ajax({
       type: 'POST',
       url: '/_api/pages.revertRemove',
       data: $('#revert-delete-page-form').serialize(),
-      dataType: 'json'
-    }).done(function(res) {
+      dataType: 'json',
+    }).done((res) => {
       // error
       if (!res.ok) {
         $('#putBackPage .msg').hide();
@@ -471,43 +469,43 @@ $(function() {
 
     return false;
   });
-  $('#unlink-page-form').submit(function(e) {
+  $('#unlink-page-form').submit((e) => {
     $.ajax({
       type: 'POST',
       url: '/_api/pages.unlink',
       data: $('#unlink-page-form').serialize(),
-      dataType: 'json'
+      dataType: 'json',
     })
-    .done(function(res) {
-      if (!res.ok) {
-        $('#delete-errors').html('<i class="fa fa-times-circle"></i> ' + res.error);
-        $('#delete-errors').addClass('alert-danger');
-      }
-      else {
-        top.location.href = res.path + '?unlinked=true';
-      }
-    });
+      .done((res) => {
+        if (!res.ok) {
+          $('#delete-errors').html(`<i class="fa fa-times-circle"></i> ${res.error}`);
+          $('#delete-errors').addClass('alert-danger');
+        }
+        else {
+          top.location.href = `${res.path}?unlinked=true`;
+        }
+      });
 
     return false;
   });
 
-  $('#create-portal-button').on('click', function(e) {
+  $('#create-portal-button').on('click', (e) => {
     $('a[data-toggle="tab"][href="#edit"]').tab('show');
 
     $('body').addClass('on-edit');
     $('body').addClass('builtin-editor');
 
     const path = $('.content-main').data('path');
-    if (path != '/' && $('.content-main').data('page-id') == '') {
+    if (path !== '/' && $('.content-main').data('page-id') === '') {
       const upperPage = path.substr(0, path.length - 1);
-      $.get('/_api/pages.get', {path: upperPage}, function(res) {
+      $.get('/_api/pages.get', { path: upperPage }, (res) => {
         if (res.ok && res.page) {
           $('#portal-warning-modal').modal('show');
         }
       });
     }
   });
-  $('#portal-form-close').on('click', function(e) {
+  $('#portal-form-close').on('click', (e) => {
     $('#edit').removeClass('active');
     $('body').removeClass('on-edit');
     $('body').removeClass('builtin-editor');
@@ -519,9 +517,8 @@ $(function() {
    */
   $('#view-list .page-list-ul-flat .page-list-link').each(function() {
     const $link = $(this);
-    /* eslint-disable no-unused-vars */
+    /* eslint-disable-next-line no-unused-vars */
     const text = $link.text();
-    /* eslint-enable */
     let path = decodeURIComponent($link.data('path'));
     const shortPath = decodeURIComponent($link.data('short-path')); // convert to string
 
@@ -531,40 +528,44 @@ $(function() {
     }
 
     path = entities.encodeHTML(path);
-    const pattern = escapeStringRegexp(entities.encodeHTML(shortPath)) + '(/)?$';
+    const pattern = `${escapeStringRegexp(entities.encodeHTML(shortPath))}(/)?$`;
 
-    $link.html(path.replace(new RegExp(pattern), '<strong>' + shortPath + '$1</strong>'));
+    $link.html(path.replace(new RegExp(pattern), `<strong>${shortPath}$1</strong>`));
   });
 
   // for list page
   let growiRendererForTimeline = null;
-  $('a[data-toggle="tab"][href="#view-timeline"]').on('shown.bs.tab', function() {
+  $('a[data-toggle="tab"][href="#view-timeline"]').on('shown.bs.tab', () => {
     const isShown = $('#view-timeline').data('shown');
 
     if (growiRendererForTimeline == null) {
       const crowi = window.crowi;
       const crowiRenderer = window.crowiRenderer;
-      growiRendererForTimeline = new GrowiRenderer(crowi, crowiRenderer, {mode: 'timeline'});
+      growiRendererForTimeline = new GrowiRenderer(crowi, crowiRenderer, { mode: 'timeline' });
     }
 
-    if (isShown == 0) {
+    if (isShown === 0) {
       $('#view-timeline .timeline-body').each(function() {
         const id = $(this).attr('id');
-        const revisionBody = '#' + id + ' .revision-body';
+        const revisionBody = `#${id} .revision-body`;
         const revisionBodyElem = document.querySelector(revisionBody);
-        /* eslint-disable no-unused-vars */
-        const revisionPath = '#' + id + ' .revision-path';
-        /* eslint-enable */
+        const revisionPath = `#${id} .revision-path`; // eslint-disable-line no-unused-vars
         const timelineElm = document.getElementById(id);
         const pageId = timelineElm.getAttribute('data-page-id');
         const pagePath = timelineElm.getAttribute('data-page-path');
         const revisionId = timelineElm.getAttribute('data-revision');
 
         ReactDOM.render(
-          <RevisionLoader lazy={true}
-            crowi={crowi} crowiRenderer={growiRendererForTimeline}
-            pageId={pageId} pagePath={pagePath} revisionId={revisionId} />,
-          revisionBodyElem);
+          <RevisionLoader
+            lazy
+            crowi={crowi}
+            crowiRenderer={growiRendererForTimeline}
+            pageId={pageId}
+            pagePath={pagePath}
+            revisionId={revisionId}
+          />,
+          revisionBodyElem,
+        );
       });
 
       $('#view-timeline').data('shown', 1);
@@ -572,19 +573,18 @@ $(function() {
   });
 
   if (pageId) {
-
     // for Crowi Template LangProcessor
     $('.template-create-button', $('#revision-body')).on('click', function() {
       const path = $(this).data('path');
       const templateId = $(this).data('template');
-      const template = $('#' + templateId).html();
+      const template = $(`#${templateId}`).html();
 
       crowi.saveDraft(path, template);
       top.location.href = `${path}#edit`;
     });
 
     if (!isSeen) {
-      $.post('/_api/pages.seen', {page_id: pageId}, function(res) {
+      $.post('/_api/pages.seen', { page_id: pageId }, (res) => {
         // ignore unless response has error
         if (res.ok && res.seenUser) {
           $('#content-main').data('page-is-seen', 1);
@@ -593,8 +593,10 @@ $(function() {
     }
 
     // presentation
-    let presentaionInitialized = false
-      , $b = $('body');
+    let presentaionInitialized = false;
+
+
+    const $b = $('body');
 
     $(document).on('click', '.toggle-presentation', function(e) {
       const $a = $(this);
@@ -606,80 +608,79 @@ $(function() {
         presentaionInitialized = true;
 
         $('<iframe />').attr({
-          src: $a.attr('href')
+          src: $a.attr('href'),
         }).appendTo($('#presentation-container'));
       }
-    }).on('click', '.fullscreen-layer', function() {
+    }).on('click', '.fullscreen-layer', () => {
       $b.toggleClass('overlay-on');
     });
-
   } // end if pageId
 
   // tab changing handling
-  $('a[href="#edit"]').on('show.bs.tab', function() {
+  $('a[href="#edit"]').on('show.bs.tab', () => {
     $('body').addClass('on-edit');
     $('body').addClass('builtin-editor');
   });
-  $('a[href="#edit"]').on('hide.bs.tab', function() {
+  $('a[href="#edit"]').on('hide.bs.tab', () => {
     $('body').removeClass('on-edit');
     $('body').removeClass('builtin-editor');
   });
-  $('a[href="#hackmd"]').on('show.bs.tab', function() {
+  $('a[href="#hackmd"]').on('show.bs.tab', () => {
     $('body').addClass('on-edit');
     $('body').addClass('hackmd');
   });
 
-  $('a[href="#hackmd"]').on('hide.bs.tab', function() {
+  $('a[href="#hackmd"]').on('hide.bs.tab', () => {
     $('body').removeClass('on-edit');
     $('body').removeClass('hackmd');
   });
 
   // hash handling
   if (isSavedStatesOfTabChanges) {
-    $('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', function() {
+    $('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', () => {
       window.location.hash = '#revision-history';
       window.history.replaceState('', 'History', '#revision-history');
     });
-    $('a[data-toggle="tab"][href="#edit"]').on('show.bs.tab', function() {
+    $('a[data-toggle="tab"][href="#edit"]').on('show.bs.tab', () => {
       window.location.hash = '#edit';
       window.history.replaceState('', 'Edit', '#edit');
     });
-    $('a[data-toggle="tab"][href="#hackmd"]').on('show.bs.tab', function() {
+    $('a[data-toggle="tab"][href="#hackmd"]').on('show.bs.tab', () => {
       window.location.hash = '#hackmd';
       window.history.replaceState('', 'HackMD', '#hackmd');
     });
-    $('a[data-toggle="tab"][href="#revision-body"]').on('show.bs.tab', function() {
+    $('a[data-toggle="tab"][href="#revision-body"]').on('show.bs.tab', () => {
       // couln't solve https://github.com/weseek/crowi-plus/issues/119 completely -- 2017.07.03 Yuki Takei
       window.location.hash = '#';
       window.history.replaceState('', '', location.href);
     });
   }
   else {
-    $('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', function() {
+    $('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', () => {
       window.history.replaceState('', 'History', '#revision-history');
     });
-    $('a[data-toggle="tab"][href="#edit"]').on('show.bs.tab', function() {
+    $('a[data-toggle="tab"][href="#edit"]').on('show.bs.tab', () => {
       window.history.replaceState('', 'Edit', '#edit');
     });
-    $('a[data-toggle="tab"][href="#hackmd"]').on('show.bs.tab', function() {
+    $('a[data-toggle="tab"][href="#hackmd"]').on('show.bs.tab', () => {
       window.history.replaceState('', 'HackMD', '#hackmd');
     });
-    $('a[data-toggle="tab"][href="#revision-body"]').on('show.bs.tab', function() {
-      window.history.replaceState('', '',  location.href.replace(location.hash, ''));
+    $('a[data-toggle="tab"][href="#revision-body"]').on('show.bs.tab', () => {
+      window.history.replaceState('', '', location.href.replace(location.hash, ''));
     });
     // replace all href="#edit" link behaviors
-    $(document).on('click', 'a[href="#edit"]', function() {
+    $(document).on('click', 'a[href="#edit"]', () => {
       window.location.replace('#edit');
     });
   }
 
   // focus to editor when 'shown.bs.tab' event fired
-  $('a[href="#edit"]').on('shown.bs.tab', function(e) {
+  $('a[href="#edit"]').on('shown.bs.tab', (e) => {
     Crowi.setCaretLineAndFocusToEditor();
   });
 });
 
-window.addEventListener('load', function(e) {
+window.addEventListener('load', (e) => {
   // hash on page
   if (location.hash) {
     if (location.hash === '#edit' || location.hash === '#edit-form') {
@@ -690,47 +691,47 @@ window.addEventListener('load', function(e) {
       // focus
       Crowi.setCaretLineAndFocusToEditor();
     }
-    else if (location.hash == '#hackmd') {
+    else if (location.hash === '#hackmd') {
       $('a[data-toggle="tab"][href="#hackmd"]').tab('show');
       $('body').addClass('on-edit');
       $('body').addClass('hackmd');
     }
-    else if (location.hash == '#revision-history') {
+    else if (location.hash === '#revision-history') {
       $('a[data-toggle="tab"][href="#revision-history"]').tab('show');
     }
   }
 
   const crowi = window.crowi;
-  if (crowi && crowi.users && crowi.users.length != 0) {
+  if (crowi && crowi.users && crowi.users.length !== 0) {
     const totalUsers = crowi.users.length;
     const $listLiker = $('.page-list-liker');
-    $listLiker.each(function(i, liker) {
+    $listLiker.each((i, liker) => {
       const count = $(liker).data('count') || 0;
-      if (count/totalUsers > 0.05) {
+      if (count / totalUsers > 0.05) {
         $(liker).addClass('popular-page-high');
         // 5%
       }
-      else if (count/totalUsers > 0.02) {
+      else if (count / totalUsers > 0.02) {
         $(liker).addClass('popular-page-mid');
         // 2%
       }
-      else if (count/totalUsers > 0.005) {
+      else if (count / totalUsers > 0.005) {
         $(liker).addClass('popular-page-low');
         // 0.5%
       }
     });
     const $listSeer = $('.page-list-seer');
-    $listSeer.each(function(i, seer) {
+    $listSeer.each((i, seer) => {
       const count = $(seer).data('count') || 0;
-      if (count/totalUsers > 0.10) {
+      if (count / totalUsers > 0.10) {
         // 10%
         $(seer).addClass('popular-page-high');
       }
-      else if (count/totalUsers > 0.05) {
+      else if (count / totalUsers > 0.05) {
         // 5%
         $(seer).addClass('popular-page-mid');
       }
-      else if (count/totalUsers > 0.02) {
+      else if (count / totalUsers > 0.02) {
         // 2%
         $(seer).addClass('popular-page-low');
       }
@@ -743,7 +744,7 @@ window.addEventListener('load', function(e) {
   Crowi.initAffix();
 });
 
-window.addEventListener('hashchange', function(e) {
+window.addEventListener('hashchange', (e) => {
   Crowi.unhighlightSelectedSection(Crowi.findHashFromUrl(e.oldURL));
   Crowi.highlightSelectedSection(Crowi.findHashFromUrl(e.newURL));
   Crowi.modifyScrollTop();
@@ -753,10 +754,10 @@ window.addEventListener('hashchange', function(e) {
     if (location.hash === '#edit') {
       $('a[data-toggle="tab"][href="#edit"]').tab('show');
     }
-    else if (location.hash == '#hackmd') {
+    else if (location.hash === '#hackmd') {
       $('a[data-toggle="tab"][href="#hackmd"]').tab('show');
     }
-    else if (location.hash == '#revision-history') {
+    else if (location.hash === '#revision-history') {
       $('a[data-toggle="tab"][href="#revision-history"]').tab('show');
     }
   }
@@ -790,8 +791,8 @@ window.addEventListener('keydown', (event) => {
         Crowi.handleKeyCtrlSlashHandler(event);
       }
       break;
+    default:
   }
-
 });
 
 // adjust min-height of page for print temporarily

+ 14 - 13
src/client/js/models/MarkdownTable.js

@@ -5,7 +5,7 @@ import csvToMarkdown from 'csv-to-markdown-table';
 // https://github.com/markdown-it/markdown-it/blob/d29f421927e93e88daf75f22089a3e732e195bd2/lib/rules_block/table.js#L83
 // https://regex101.com/r/7BN2fR/7
 const tableAlignmentLineRE = /^[-:|][-:|\s]*$/;
-const tableAlignmentLineNegRE = /^[^-:]*$/;  // it is need to check to ignore empty row which is matched above RE
+const tableAlignmentLineNegRE = /^[^-:]*$/; // it is need to check to ignore empty row which is matched above RE
 const linePartOfTableRE = /^\|[^\r\n]*|[^\r\n]*\|$|([^|\r\n]+\|[^|\r\n]*)+/; // own idea
 
 // set up DOMParser
@@ -33,7 +33,7 @@ export default class MarkdownTable {
    * (This method clones only the table field.)
    */
   clone() {
-    let newTable = [];
+    const newTable = [];
     for (let i = 0; i < this.table.length; i++) {
       newTable.push([].concat(this.table[i]));
     }
@@ -75,11 +75,11 @@ export default class MarkdownTable {
     const tableElement = dom.querySelector('table');
     const trElements = tableElement.querySelectorAll('tr');
 
-    let table = [];
+    const table = [];
     let maxRowSize = 0;
     for (let i = 0; i < trElements.length; i++) {
-      let row = [];
-      let cellElements = trElements[i].querySelectorAll('th,td');
+      const row = [];
+      const cellElements = trElements[i].querySelectorAll('th,td');
       for (let j = 0; j < cellElements.length; j++) {
         row.push(cellElements[j].innerHTML);
       }
@@ -88,12 +88,12 @@ export default class MarkdownTable {
       if (maxRowSize < row.length) maxRowSize = row.length;
     }
 
-    let align = [];
+    const align = [];
     for (let i = 0; i < maxRowSize; i++) {
       align.push('');
     }
 
-    return new MarkdownTable(table, {align: align});
+    return new MarkdownTable(table, { align });
   }
 
   /**
@@ -110,7 +110,7 @@ export default class MarkdownTable {
    */
   static fromMarkdownString(str) {
     const arrMDTableLines = str.split(/(\r\n|\r|\n)/);
-    let contents = [];
+    const contents = [];
     let aligns = [];
     for (let n = 0; n < arrMDTableLines.length; n++) {
       const line = arrMDTableLines[n];
@@ -119,15 +119,15 @@ export default class MarkdownTable {
         // parse line which described alignment
         const alignRuleRE = [
           { align: 'c', regex: /^:-+:$/ },
-          { align: 'l', regex: /^:-+$/  },
-          { align: 'r', regex: /^-+:$/  },
+          { align: 'l', regex: /^:-+$/ },
+          { align: 'r', regex: /^-+:$/ },
         ];
         let lineText = '';
         lineText = line.replace(/^\||\|$/g, ''); // strip off pipe charactor which is placed head of line and last of line.
         lineText = lineText.replace(/\s*/g, '');
-        aligns = lineText.split(/\|/).map(col => {
-          const rule = alignRuleRE.find(rule => col.match(rule.regex));
-          return (rule != undefined) ? rule.align : '';
+        aligns = lineText.split(/\|/).map((col) => {
+          const rule = alignRuleRE.find((rule) => { return col.match(rule.regex) });
+          return (rule != null) ? rule.align : '';
         });
       }
       else if (linePartOfTableRE.test(line)) {
@@ -141,4 +141,5 @@ export default class MarkdownTable {
     }
     return (new MarkdownTable(contents, { align: aligns, stringLength: stringWidth }));
   }
+
 }

+ 1 - 1
src/client/js/plugin.js

@@ -39,4 +39,4 @@ export default class CrowiPlugin {
 
 }
 
-window.crowiPlugin = new CrowiPlugin();     // FIXME
+window.crowiPlugin = new CrowiPlugin(); // FIXME

+ 44 - 44
src/client/js/util/Crowi.js

@@ -15,6 +15,7 @@ import {
 } from './interceptor/detach-code-blocks';
 
 export default class Crowi {
+
   constructor(context, window) {
     this.context = context;
     this.config = {};
@@ -36,8 +37,8 @@ export default class Crowi {
     this.apiRequest = this.apiRequest.bind(this);
 
     this.interceptorManager = new InterceptorManager();
-    this.interceptorManager.addInterceptor(new DetachCodeBlockInterceptor(this), 10);       // process as soon as possible
-    this.interceptorManager.addInterceptor(new RestoreCodeBlockInterceptor(this), 900);     // process as late as possible
+    this.interceptorManager.addInterceptor(new DetachCodeBlockInterceptor(this), 10); // process as soon as possible
+    this.interceptorManager.addInterceptor(new RestoreCodeBlockInterceptor(this), 900); // process as late as possible
 
     // FIXME
     this.me = context.me;
@@ -46,7 +47,7 @@ export default class Crowi {
 
     this.users = [];
     this.userByName = {};
-    this.userById   = {};
+    this.userById = {};
     this.draft = {};
     this.editorOptions = {};
 
@@ -100,7 +101,7 @@ export default class Crowi {
       'previewOptions',
     ];
 
-    keys.forEach(key => {
+    keys.forEach((key) => {
       const keyContent = this.localStorage[key];
       if (keyContent) {
         try {
@@ -114,35 +115,36 @@ export default class Crowi {
   }
 
   fetchUsers() {
-    const interval = 1000*60*15; // 15min
+    const interval = 1000 * 60 * 15; // 15min
     const currentTime = new Date();
     if (this.localStorage.lastFetched && interval > currentTime - new Date(this.localStorage.lastFetched)) {
-      return ;
+      return;
     }
 
     this.apiGet('/users.list', {})
-    .then(data => {
-      this.users = data.users;
-      this.localStorage.users = JSON.stringify(data.users);
-
-      let userByName = {};
-      let userById = {};
-      for (let i = 0; i < data.users.length; i++) {
-        const user = data.users[i];
-        userByName[user.username] = user;
-        userById[user._id] = user;
-      }
-      this.userByName = userByName;
-      this.localStorage.userByName = JSON.stringify(userByName);
+      .then((data) => {
+        this.users = data.users;
+        this.localStorage.users = JSON.stringify(data.users);
+
+        const userByName = {};
+        const userById = {};
+        for (let i = 0; i < data.users.length; i++) {
+          const user = data.users[i];
+          userByName[user.username] = user;
+          userById[user._id] = user;
+        }
+        this.userByName = userByName;
+        this.localStorage.userByName = JSON.stringify(userByName);
 
-      this.userById = userById;
-      this.localStorage.userById = JSON.stringify(userById);
+        this.userById = userById;
+        this.localStorage.userById = JSON.stringify(userById);
 
-      this.localStorage.lastFetched = new Date();
-    }).catch(err => {
-      this.localStorage.removeItem('lastFetched');
+        this.localStorage.lastFetched = new Date();
+      })
+      .catch((err) => {
+        this.localStorage.removeItem('lastFetched');
       // ignore errors
-    });
+      });
   }
 
   setCaretLine(line) {
@@ -192,9 +194,9 @@ export default class Crowi {
   }
 
   findUserByIds(userIds) {
-    let users = [];
-    for (let userId of userIds) {
-      let user = this.findUserById(userId);
+    const users = [];
+    for (const userId of userIds) {
+      const user = this.findUserById(userId);
       if (user) {
         users.push(user);
       }
@@ -217,7 +219,7 @@ export default class Crowi {
       body: markdown,
     });
     return this.apiPost('/pages.create', params)
-      .then(res => {
+      .then((res) => {
         if (!res.ok) {
           throw new Error(res.error);
         }
@@ -232,7 +234,7 @@ export default class Crowi {
       body: markdown,
     });
     return this.apiPost('/pages.update', params)
-      .then(res => {
+      .then((res) => {
         if (!res.ok) {
           throw new Error(res.error);
         }
@@ -251,7 +253,7 @@ export default class Crowi {
   }
 
   apiGet(path, params) {
-    return this.apiRequest('get', path, {params: params});
+    return this.apiRequest('get', path, { params });
   }
 
   apiPost(path, params) {
@@ -263,22 +265,20 @@ export default class Crowi {
   }
 
   apiRequest(method, path, params) {
-
     return new Promise((resolve, reject) => {
       axios[method](`/_api${path}`, params)
-      .then(res => {
-        if (res.data.ok) {
-          resolve(res.data);
-        }
-        else {
-          reject(new Error(res.data.error));
-        }
-      })
-      .catch(res => {
-        reject(res);
-      });
+        .then((res) => {
+          if (res.data.ok) {
+            resolve(res.data);
+          }
+          else {
+            reject(new Error(res.data.error));
+          }
+        })
+        .catch((res) => {
+          reject(res);
+        });
     });
   }
 
 }
-

+ 17 - 14
src/client/js/util/GrowiRenderer.js

@@ -1,8 +1,8 @@
 import MarkdownIt from 'markdown-it';
 
-import Linker        from './PreProcessor/Linker';
-import CsvToTable    from './PreProcessor/CsvToTable';
-import XssFilter     from './PreProcessor/XssFilter';
+import Linker from './PreProcessor/Linker';
+import CsvToTable from './PreProcessor/CsvToTable';
+import XssFilter from './PreProcessor/XssFilter';
 import CrowiTemplate from './PostProcessor/CrowiTemplate';
 
 import EmojiConfigurer from './markdown-it/emoji';
@@ -32,9 +32,10 @@ export default class GrowiRenderer {
   constructor(crowi, originRenderer, options) {
     this.crowi = crowi;
     this.originRenderer = originRenderer || {};
-    this.options = Object.assign( // merge options
-      { isAutoSetup: true },      // default options
-      options || {});             // specified options
+    this.options = Object.assign( //  merge options
+      { isAutoSetup: true }, //       default options
+      options || {}, //               specified options
+    );
 
     // initialize processors
     //  that will be retrieved if originRenderer exists
@@ -89,21 +90,21 @@ export default class GrowiRenderer {
           new TocAndAnchorConfigurer(crowi, options.renderToc),
           new HeaderLineNumberConfigurer(crowi),
           new HeaderWithEditLinkConfigurer(crowi),
-          new TableWithHandsontableButtonConfigurer(crowi)
+          new TableWithHandsontableButtonConfigurer(crowi),
         ]);
         break;
       case 'editor':
         this.markdownItConfigurers = this.markdownItConfigurers.concat([
           new FooternoteConfigurer(crowi),
           new HeaderLineNumberConfigurer(crowi),
-          new TableConfigurer(crowi)
+          new TableConfigurer(crowi),
         ]);
         break;
       // case 'comment':
       //   break;
       default:
         this.markdownItConfigurers = this.markdownItConfigurers.concat([
-          new TableConfigurer(crowi)
+          new TableConfigurer(crowi),
         ]);
         break;
     }
@@ -115,7 +116,7 @@ export default class GrowiRenderer {
   setup() {
     const crowiConfig = this.crowi.config;
 
-    let isEnabledLinebreaks = undefined;
+    let isEnabledLinebreaks;
     switch (this.options.mode) {
       case 'comment':
         isEnabledLinebreaks = crowiConfig.isEnabledLinebreaksInComments;
@@ -137,14 +138,15 @@ export default class GrowiRenderer {
   }
 
   preProcess(markdown) {
+    let processed = markdown;
     for (let i = 0; i < this.preProcessors.length; i++) {
       if (!this.preProcessors[i].process) {
         continue;
       }
-      markdown = this.preProcessors[i].process(markdown);
+      processed = this.preProcessors[i].process(markdown);
     }
 
-    return markdown;
+    return processed;
   }
 
   process(markdown) {
@@ -152,14 +154,15 @@ export default class GrowiRenderer {
   }
 
   postProcess(html) {
+    let processed = html;
     for (let i = 0; i < this.postProcessors.length; i++) {
       if (!this.postProcessors[i].process) {
         continue;
       }
-      html = this.postProcessors[i].process(html);
+      processed = this.postProcessors[i].process(html);
     }
 
-    return html;
+    return processed;
   }
 
   codeRenderer(code, langExt) {

+ 13 - 13
src/client/js/util/PostProcessor/CrowiTemplate.js

@@ -4,10 +4,10 @@ export default class CrowiTemplate {
 
   constructor(crowi) {
     this.templatePattern = {
-      'year': this.getYear,
-      'month': this.getMonth,
-      'date': this.getDate,
-      'user': this.getUser,
+      year: this.getYear,
+      month: this.getMonth,
+      date: this.getDate,
+      user: this.getUser,
     };
   }
 
@@ -30,13 +30,13 @@ export default class CrowiTemplate {
 
       return (
         /* eslint-disable quotes */
-        `<div class="page-template-builder">` +
-          `<button class="template-create-button btn btn-default" data-template="${templateId}" data-path="${pageName}">` +
-            `<i class="fa fa-pencil"></i> ${pageName}` +
-          `</button>` +
-          `<pre><code id="${templateId}" class="lang-${lang}">${code}\n</code></pre>` +
-        `</div>`
-        /* eslint-enable */
+        `<div class="page-template-builder">`
+          + `<button class="template-create-button btn btn-default" data-template="${templateId}" data-path="${pageName}">`
+            + `<i class="fa fa-pencil"></i> ${pageName}`
+          + `</button>`
+          + `<pre><code id="${templateId}" class="lang-${lang}">${code}\n</code></pre>`
+        + `</div>`
+        /* eslint-enable quotes */
       );
     });
   }
@@ -67,8 +67,8 @@ export default class CrowiTemplate {
   parseTemplateString(templateString) {
     let parsed = templateString;
 
-    Object.keys(this.templatePattern).forEach(key => {
-      const k = key .replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+    Object.keys(this.templatePattern).forEach((key) => {
+      const k = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
       const matcher = new RegExp(`{${k}}`, 'g');
       if (parsed.match(matcher)) {
         const replacer = this.templatePattern[key]();

+ 2 - 1
src/client/js/util/PreProcessor/CsvToTable.js

@@ -1,8 +1,8 @@
 import csvToMarkdownTable from 'csv-to-markdown-table';
 
 export default class CsvToTable {
-  process(markdown) {
 
+  process(markdown) {
     // see: https://regex101.com/r/WR6IvX/3
     return markdown.replace(/:::\s*(\S+)[\r\n]((.|[\r\n])*?)[\r\n]:::/gm, (all, group1, group2) => {
       switch (group1) {
@@ -19,4 +19,5 @@ export default class CsvToTable {
       }
     });
   }
+
 }

+ 3 - 3
src/client/js/util/PreProcessor/Linker.js

@@ -1,14 +1,14 @@
 
 export default class Linker {
-  process(markdown) {
 
+  process(markdown) {
     return markdown
       // process angle branckets like '</Level1/Level2>'
       // see: https://regex101.com/r/rxAy4F/2
       .replace(/<((\/[^>\n]+?){2,})>/g, '<a href="$1">$1</a>') // ページ間リンク: <> でかこまれてて / から始まり、 / が2個以上
       // process square branckets like '[/Level1]'
       // see: https://regex101.com/r/QSt1yu/5
-      .replace(/\[(\/[^\]\n]+?)\](?!\()/g, '<a href="$1">$1</a>')
-    ;
+      .replace(/\[(\/[^\]\n]+?)\](?!\()/g, '<a href="$1">$1</a>');
   }
+
 }

+ 4 - 5
src/client/js/util/PreProcessor/XssFilter.js

@@ -1,5 +1,5 @@
 import Xss from '@commons/service/xss';
-import xssOption from '@commons/service/xss/xssOption';
+import XssOption from '@commons/service/xss/xssOption';
 
 export default class XssFilter {
 
@@ -7,7 +7,7 @@ export default class XssFilter {
     this.crowi = crowi;
 
     if (crowi.config.isEnabledXssPrevention) {
-      this.xssOption = new xssOption(crowi.config);
+      this.xssOption = new XssOption(crowi.config);
       this.xss = new Xss(this.xssOption);
     }
   }
@@ -16,9 +16,8 @@ export default class XssFilter {
     if (this.crowi.config.isEnabledXssPrevention) {
       return this.xss.process(markdown);
     }
-    else {
-      return markdown;
-    }
+
+    return markdown;
   }
 
 }

+ 1 - 1
src/client/js/util/codemirror/update-display-util.ext.js

@@ -21,7 +21,7 @@ class UpdateDisplayUtil {
     const update = new DisplayUpdate(cm, cm.getViewport());
 
     // Compute a suitable new viewport (from & to)
-    let end = doc.first + doc.size;
+    const end = doc.first + doc.size;
     let from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first);
     let to = Math.min(end, update.visible.to + cm.options.viewportMargin);
     if (display.viewFrom < from && from - display.viewFrom < 20) from = Math.max(doc.first, display.viewFrom);

+ 14 - 11
src/client/js/util/interceptor/detach-code-blocks.js

@@ -2,9 +2,11 @@ import { BasicInterceptor } from 'growi-pluginkit';
 
 
 class DetachCodeBlockUtil {
+
   static createReplaceStr(replaceId) {
     return `<pre class="detached-code-block">${replaceId}</pre>`;
   }
+
 }
 
 /**
@@ -31,7 +33,7 @@ export class DetachCodeBlockInterceptor extends BasicInterceptor {
     if (contextName === 'prePreProcess') {
       return 'markdown';
     }
-    else if (contextName === 'prePostProcess') {
+    if (contextName === 'prePostProcess') {
       return 'parsedHTML';
     }
   }
@@ -42,22 +44,21 @@ export class DetachCodeBlockInterceptor extends BasicInterceptor {
   process(contextName, ...args) {
     this.logger.debug(`processing: 'contextName'=${contextName}`);
 
-    const context = Object.assign(args[0]);   // clone
+    const context = Object.assign(args[0]); // clone
     const targetKey = this.getTargetKey(contextName);
-    /* eslint-disable no-unused-vars */
-    const currentPagePath = context.currentPagePath;
-    /* eslint-enable */
+    const currentPagePath = context.currentPagePath; // eslint-disable-line no-unused-vars
 
     context.dcbContextMap = {};
 
     // see: https://regex101.com/r/8PAEcC/5
+    // eslint-disable-next-line max-len
     context[targetKey] = context[targetKey].replace(/(^(```|~~~)(.|[\r\n])*?(```|~~~)$)|(`[^\r\n]*?`)|(<pre>(.|[\r\n])*?<\/pre>)|(<pre\s[^>]*>(.|[\r\n])*?<\/pre>)/gm, (all) => {
       // create ID
-      const replaceId = 'dcb-' + this.createRandomStr(8);
+      const replaceId = `dcb-${this.createRandomStr(8)}`;
       this.logger.debug(`'replaceId'=${replaceId} : `, all);
 
       // register to context
-      let dcbContext = {};
+      const dcbContext = {};
       dcbContext.content = all;
       dcbContext.substituteContent = DetachCodeBlockUtil.createReplaceStr(replaceId);
       context.dcbContextMap[replaceId] = dcbContext;
@@ -79,11 +80,12 @@ export class DetachCodeBlockInterceptor extends BasicInterceptor {
   createRandomStr(length) {
     const bag = 'abcdefghijklmnopqrstuvwxyz0123456789';
     let generated = '';
-    for (var i = 0; i < length; i++) {
+    for (let i = 0; i < length; i++) {
       generated += bag[Math.floor(Math.random() * bag.length)];
     }
     return generated;
   }
+
 }
 
 
@@ -111,7 +113,7 @@ export class RestoreCodeBlockInterceptor extends BasicInterceptor {
     if (contextName === 'postPreProcess') {
       return 'markdown';
     }
-    else if (contextName === 'preRenderHtml' || contextName === 'preRenderPreviewHtml'
+    if (contextName === 'preRenderHtml' || contextName === 'preRenderPreviewHtml'
         || contextName === 'preRenderCommentHtml' || contextName === 'preRenderCommentPreviewHtml') {
       return 'parsedHTML';
     }
@@ -123,13 +125,13 @@ export class RestoreCodeBlockInterceptor extends BasicInterceptor {
   process(contextName, ...args) {
     this.logger.debug(`processing: 'contextName'=${contextName}`);
 
-    const context = Object.assign(args[0]);   // clone
+    const context = Object.assign(args[0]); // clone
     const targetKey = this.getTargetKey(contextName);
 
     // forEach keys of dcbContextMap
     Object.keys(context.dcbContextMap).forEach((replaceId) => {
       // get context object from context
-      let dcbContext = context.dcbContextMap[replaceId];
+      const dcbContext = context.dcbContextMap[replaceId];
 
       // replace it with content by using getter function so that the doller sign does not work
       // see: https://github.com/weseek/growi/issues/285
@@ -139,4 +141,5 @@ export class RestoreCodeBlockInterceptor extends BasicInterceptor {
     // resolve
     return Promise.resolve(context);
   }
+
 }

+ 1 - 0
src/client/js/util/markdown-it/blockdiag.js

@@ -13,4 +13,5 @@ export default class BlockdiagConfigurer {
       marker: ':::',
     });
   }
+
 }

+ 4 - 2
src/client/js/util/markdown-it/emoji.js

@@ -9,13 +9,15 @@ export default class EmojiConfigurer {
 
     const emojiShortnameUnicodeMap = {};
 
-    for (let unicode in emojiStrategy) {
+    /* eslint-disable guard-for-in, no-restricted-syntax */
+    for (const unicode in emojiStrategy) {
       const data = emojiStrategy[unicode];
       const shortname = data.shortname.replace(/:/g, '');
       emojiShortnameUnicodeMap[shortname] = String.fromCharCode(unicode);
     }
+    /* eslint-enable guard-for-in, no-restricted-syntax */
 
-    md.use(require('markdown-it-emoji'), {defs: emojiShortnameUnicodeMap});
+    md.use(require('markdown-it-emoji'), { defs: emojiShortnameUnicodeMap });
 
     // integrate markdown-it-emoji and emojione
     md.renderer.rules.emoji = (token, idx) => {

+ 3 - 3
src/client/js/util/markdown-it/header-line-number.js

@@ -27,9 +27,9 @@ export default class HeaderLineNumberConfigurer {
       if (original) {
         return original(tokens, idx, options, env, self);
       }
-      else {
-        return self.renderToken(tokens, idx, options, env, self);
-      }
+
+      return self.renderToken(tokens, idx, options, env, self);
     };
   }
+
 }

+ 1 - 0
src/client/js/util/markdown-it/header-with-edit-link.js

@@ -13,4 +13,5 @@ export default class HeaderWithEditLinkConfigurer {
               </span></${tokens[idx].tag}>`;
     };
   }
+
 }

+ 3 - 3
src/client/js/util/markdown-it/header.js

@@ -18,9 +18,8 @@ export default class HeaderConfigurer {
       if (original) {
         return original(tokens, idx, options, env, self);
       }
-      else {
-        return self.renderToken(tokens, idx, options, env, self);
-      }
+
+      return self.renderToken(tokens, idx, options, env, self);
     };
   }
 
@@ -32,4 +31,5 @@ export default class HeaderConfigurer {
       tokens[idx].attrJoin('class', 'revision-head');
     }
   }
+
 }

+ 1 - 1
src/client/js/util/markdown-it/mathjax.js

@@ -4,7 +4,7 @@ export default class MathJaxConfigurer {
     this.crowi = crowi;
 
     const config = crowi.getConfig();
-    this.isEnabled = !!config.env.MATHJAX;  // convert to boolean
+    this.isEnabled = !!config.env.MATHJAX; // convert to boolean
   }
 
   configure(md) {

+ 1 - 0
src/client/js/util/markdown-it/plantuml.js

@@ -22,4 +22,5 @@ export default class PlantUMLConfigurer {
     const zippedCode = plantumlEncoder.encode(`@startuml\n${umlCode}\n@enduml`);
     return urljoin(this.serverUrl, 'svg', zippedCode);
   }
+
 }

+ 3 - 1
src/client/js/util/markdown-it/table-with-handsontable-button.js

@@ -7,7 +7,8 @@ export default class TableWithHandsontableButtonConfigurer {
   configure(md) {
     md.renderer.rules.table_open = (tokens, idx) => {
       const beginLine = tokens[idx].map[0] + 1;
-      const endLine  = tokens[idx].map[1];
+      const endLine = tokens[idx].map[1];
+      // eslint-disable-next-line max-len
       return `<div class="editable-with-handsontable"><button class="handsontable-modal-trigger" onClick="crowi.launchHandsontableModal('page', ${beginLine}, ${endLine})"><i class="icon-note"></i></button><table class="table table-bordered">`;
     };
 
@@ -15,4 +16,5 @@ export default class TableWithHandsontableButtonConfigurer {
       return '</table></div>';
     };
   }
+
 }

+ 37 - 34
src/client/js/util/reveal/plugins/growi-renderer.js

@@ -5,17 +5,20 @@ import GrowiRenderer from '../../GrowiRenderer';
  */
 (function(root, factory) {
   // parent window DOM (crowi.js) of presentation window.
-  let parentWindow = window.parent;
+  const parentWindow = window.parent;
 
   // create GrowiRenderer instance and setup.
-  let growiRenderer = new GrowiRenderer(parentWindow.crowi, parentWindow.crowiRenderer, {mode: 'editor'});
+  const growiRenderer = new GrowiRenderer(parentWindow.crowi, parentWindow.crowiRenderer, { mode: 'editor' });
 
-  let growiRendererPlugin = factory(growiRenderer);
+  const growiRendererPlugin = factory(growiRenderer);
   growiRendererPlugin.initialize();
-}(this, function(growiRenderer) {
-  const DEFAULT_SLIDE_SEPARATOR = '^\r?\n---\r?\n$',
-    DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR = '\\\.element\\\s*?(.+?)$',
-    DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR = '\\\.slide:\\\s*?(\\\S.+?)$';
+}(this, (growiRenderer) => {
+  /* eslint-disable no-useless-escape */
+  const DEFAULT_SLIDE_SEPARATOR = '^\r?\n---\r?\n$';
+  const DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR = '\\\.element\\\s*?(.+?)$';
+  const DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR = '\\\.slide:\\\s*?(\\\S.+?)$';
+  /* eslint-enable no-useless-escape */
+
   let marked;
 
   /**
@@ -23,16 +26,16 @@ import GrowiRenderer from '../../GrowiRenderer';
    * starting with '#' to markdown.
    */
   function divideSlides() {
-    let sections = document.querySelectorAll('[data-markdown]');
+    const sections = document.querySelectorAll('[data-markdown]');
     for (let i = 0, len = sections.length; i < len; i++) {
-      let section = sections[i];
-      let markdown = marked.getMarkdownFromSlide(section);
-      let context = {markdown};
+      const section = sections[i];
+      const markdown = marked.getMarkdownFromSlide(section);
+      const context = { markdown };
       const interceptorManager = growiRenderer.crowi.interceptorManager;
-      let dataSeparator = section.getAttribute( 'data-separator' ) || DEFAULT_SLIDE_SEPARATOR;
+      let dataSeparator = section.getAttribute('data-separator') || DEFAULT_SLIDE_SEPARATOR;
       // replace string '\n' to LF code.
       dataSeparator = dataSeparator.replace(/\\n/g, '\n');
-      const replaceValue = dataSeparator + '#';
+      const replaceValue = `${dataSeparator}#`;
       // detach code block.
       interceptorManager.process('prePreProcess', context);
       // if there is only '\n' in the first line, replace it.
@@ -49,50 +52,50 @@ import GrowiRenderer from '../../GrowiRenderer';
    * Converts data-markdown slides to HTML slides by GrowiRenderer.
    */
   function convertSlides() {
-    let sections = document.querySelectorAll('[data-markdown]');
+    const sections = document.querySelectorAll('[data-markdown]');
     let markdown;
     const interceptorManager = growiRenderer.crowi.interceptorManager;
 
     for (let i = 0, len = sections.length; i < len; i++) {
-      let section = sections[i];
+      const section = sections[i];
 
       // Only parse the same slide once
       if (!section.getAttribute('data-markdown-parsed')) {
         section.setAttribute('data-markdown-parsed', 'true');
-        let notes = section.querySelector( 'aside.notes' );
+        const notes = section.querySelector('aside.notes');
         markdown = marked.getMarkdownFromSlide(section);
-        let context = { markdown };
+        const context = { markdown };
 
         interceptorManager.process('preRender', context)
-          .then(() => interceptorManager.process('prePreProcess', context))
+          .then(() => { return interceptorManager.process('prePreProcess', context) })
           .then(() => {
             context.markdown = growiRenderer.preProcess(context.markdown);
           })
-          .then(() => interceptorManager.process('postPreProcess', context))
+          .then(() => { return interceptorManager.process('postPreProcess', context) })
           .then(() => {
-            context['parsedHTML'] = growiRenderer.process(context.markdown);
+            context.parsedHTML = growiRenderer.process(context.markdown);
           })
-          .then(() => interceptorManager.process('prePostProcess', context))
+          .then(() => { return interceptorManager.process('prePostProcess', context) })
           .then(() => {
             context.parsedHTML = growiRenderer.postProcess(context.parsedHTML);
           })
-          .then(() => interceptorManager.process('postPostProcess', context))
-          .then(() => interceptorManager.process('preRenderHtml', context))
-          .then(() => interceptorManager.process('postRenderHtml', context))
+          .then(() => { return interceptorManager.process('postPostProcess', context) })
+          .then(() => { return interceptorManager.process('preRenderHtml', context) })
+          .then(() => { return interceptorManager.process('postRenderHtml', context) })
           .then(() => {
             section.innerHTML = context.parsedHTML;
           });
-        marked.addAttributes(   section, section, null, section.getAttribute( 'data-element-attributes' ) ||
-          section.parentNode.getAttribute( 'data-element-attributes' ) ||
-          DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR,
-        section.getAttribute( 'data-attributes' ) ||
-          section.parentNode.getAttribute( 'data-attributes' ) ||
-          DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR);
+        marked.addAttributes(section, section, null, section.getAttribute('data-element-attributes')
+          || section.parentNode.getAttribute('data-element-attributes')
+          || DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR,
+        section.getAttribute('data-attributes')
+          || section.parentNode.getAttribute('data-attributes')
+          || DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR);
 
         // If there were notes, we need to re-add them after
         // having overwritten the section's HTML
-        if ( notes ) {
-          section.appendChild( notes );
+        if (notes) {
+          section.appendChild(notes);
         }
       }
     }
@@ -100,12 +103,12 @@ import GrowiRenderer from '../../GrowiRenderer';
 
   // API
   return {
-    initialize: async function() {
+    async initialize() {
       growiRenderer.setup();
       marked = require('./markdown').default(growiRenderer.process);
       divideSlides();
       marked.processSlides();
       convertSlides();
-    }
+    },
   };
 }));

+ 15 - 16
src/client/styles/scss/_admin.scss

@@ -1,17 +1,16 @@
 .admin-page {
-
   .admin-user-menu {
     .dropdown-menu {
-      left: auto;
       right: 0;
+      left: auto;
       width: 300px;
     }
-   }
+  }
 
   .admin-group-menu {
     .dropdown-menu {
-      left: auto;
       right: 0;
+      left: auto;
     }
   }
 
@@ -19,7 +18,7 @@
     @import 'hljs';
 
     .ss-container img {
-      padding: .5em;
+      padding: 0.5em;
       background-color: #ddd;
     }
 
@@ -32,9 +31,9 @@
 
   .admin-security {
     .passport-logo {
-      margin-top: -.5em;
-      padding: 3px;
       height: 32px;
+      padding: 3px;
+      margin-top: -0.5em;
       background-color: black;
     }
 
@@ -45,21 +44,21 @@
 
   .admin-notification {
     .td-abs-center {
+      width: 1px; // to keep the cell small
       text-align: center;
       vertical-align: middle;
-      width: 1px; // to keep the cell small
     }
   }
 
   .admin-importer {
     table.table-mapping {
-      th, td {
+      th,
+      td {
         text-align: center;
       }
     }
   }
 
-
   // Toggle Twitter Bootstrap button class when active
   // https://jsfiddle.net/ms040m01/3/
   @mixin active-color($color, $bg-color, $border-color) {
@@ -75,18 +74,18 @@
     .btn {
       min-width: 60px;
     }
-    .btn.active[data-active-class="default"] {
+    .btn.active[data-active-class='default'] {
       @include active-color($btn-default-color, $btn-default-bg, $btn-default-border);
     }
-    .btn.active[data-active-class="primary"] {
+    .btn.active[data-active-class='primary'] {
       @include active-color($btn-primary-color, $btn-primary-bg, $btn-primary-border);
     }
 
     // disabled btn-group styles
     &.btn-group-disabled {
       .btn:hover {
-        background-color: unset;
         cursor: not-allowed;
+        background-color: unset;
       }
     }
   }
@@ -97,9 +96,9 @@
     .theme-option-container {
       min-width: 100px;
       a {
+        padding: 3px;
         margin-right: 10px;
         margin-bottom: 10px;
-        padding: 3px;
 
         svg {
           display: block;
@@ -108,14 +107,14 @@
     }
 
     &.disabled {
-      opacity: 0.5;
       cursor: not-allowed;
+      opacity: 0.5;
     }
 
     // style
     .theme-option-container a {
-      border: 1px solid #ccc;
       background-color: #f5f5f5;
+      border: 1px solid #ccc;
     }
     .theme-option-name {
       opacity: 0.3;

+ 8 - 7
src/client/styles/scss/_attachments.scss

@@ -1,39 +1,40 @@
 .page-attachments-row {
   border-top: solid 1px transparent;
 }
+
 .page-attachments {
   li.attachment {
     list-style: none;
   }
 }
-.page-attachments, .page-meta {
+
+.page-attachments,
+.page-meta {
   font-size: 0.95em;
 
   .attachment-in-use {
-    margin: 0 0 0 4px;
     padding: 1px 5px;
+    margin: 0 0 0 4px;
   }
 
   .attachment-filetype {
-    margin: 0 0 0 4px;
     padding: 1px 5px;
+    margin: 0 0 0 4px;
     font-weight: normal;
   }
 
   .attachment-download {
-    cursor: pointer;
     margin: 0 0 0 4px;
+    cursor: pointer;
   }
 
   .attachment-delete {
-    cursor: pointer;
     margin: 0 0 0 4px;
+    cursor: pointer;
   }
-
 }
 
 .attachment-delete-modal {
-
   .attachment-delete-image {
     text-align: center;
 

+ 5 - 6
src/client/styles/scss/_comment.scss

@@ -3,9 +3,9 @@
     // delete button
     .page-comment-control {
       position: absolute;
-      display: none;    // default hidden
       top: 0;
       right: 0;
+      display: none; // default hidden
     }
   }
 
@@ -13,9 +13,9 @@
   .page-comment-delete-modal .modal-content {
     .modal-body {
       .comment-body {
+        max-height: 13em;
         // scrollable
         overflow-y: auto;
-        max-height: 13em;
       }
     }
   }
@@ -23,14 +23,13 @@
 
 .main-container {
   .page-comments {
-
     .page-comments-list-toggle-newer,
     .page-comments-list-toggle-older {
-      text-align: center;
       display: block;
       margin: 8px;
-      font-size: .9em;
+      font-size: 0.9em;
       color: #999;
+      text-align: center;
     }
 
     // older comments
@@ -38,7 +37,7 @@
     }
     // newer comments
     .page-comments-list-newer .page-comment {
-      opacity: .7;
+      opacity: 0.7;
 
       &:hover {
         opacity: 1;

+ 14 - 15
src/client/styles/scss/_comment_growi.scss

@@ -1,10 +1,9 @@
-.growi.main-container  {
-
+.growi.main-container {
   %comment-section {
     position: relative;
     padding: 1em;
-    margin-left: 4.5em;
     margin-bottom: 1em;
+    margin-left: 4.5em;
     // screen-xs
     @media (max-width: $screen-xs) {
       margin-left: 3.5em;
@@ -12,14 +11,14 @@
 
     // speech balloon
     &:before {
-      border: 1em solid transparent;
-      border-left-width: 0;
+      position: absolute;
+      top: 1.5em;
       left: -1em;
-      content: "";
       display: block;
-      top: 1.5em;
-      position: absolute;
       width: 0;
+      content: '';
+      border: 1em solid transparent;
+      border-left-width: 0;
 
       // screen-xs
       @media (max-width: $screen-xs) {
@@ -30,9 +29,9 @@
 
   %picture {
     float: left;
-    margin-top: 0.8em;
     width: 3em;
     height: 3em;
+    margin-top: 0.8em;
     // screen-xs
     @media (max-width: $screen-xs) {
       width: 2em;
@@ -41,8 +40,8 @@
   }
 
   .page-comments-row {
-    border-top: 5px solid;
     margin: 30px 0px;
+    border-top: 5px solid;
   }
 
   .page-comments {
@@ -78,8 +77,8 @@
     }
 
     .page-comment-meta {
+      font-size: 0.9em;
       color: #999;
-      font-size: .9em;
       text-align: right;
 
       * {
@@ -96,10 +95,9 @@
   // display cheatsheet for comment form only
   .comment-form {
     .editor-cheatsheet {
-        display: none;
+      display: none;
     }
 
-
     position: relative;
     margin-top: 2em;
     // user icon
@@ -118,9 +116,10 @@
     }
     .comment-form-comment {
       height: 80px;
-      &:focus, &:not(:invalid) {
-        transition: height 0.2s ease-out;
+      &:focus,
+      &:not(:invalid) {
         height: 180px;
+        transition: height 0.2s ease-out;
       }
     }
     .comment-form-preview {

+ 18 - 19
src/client/styles/scss/_comment_kibela.scss

@@ -1,28 +1,27 @@
-
 .kibela.main-container {
   /* Comment section */
   %comment-section {
     position: relative;
     padding: 1em;
-    margin-left: 4.5em;
     margin-bottom: 1em; // screen-xs
+    margin-left: 4.5em;
     @media (max-width: $screen-xs) {
       margin-left: 3.5em;
     } // speech balloon
     &:before {
+      position: absolute;
+      top: 1.5em;
+      left: -1em;
+      display: block;
       width: 0;
+      width: 0; // screen-xs
       height: 0;
+      content: '';
       border-top: 20px solid transparent;
-      border-left: 20px solid transparent;
+      border-right: 20px solid #e6e9ec;
       border-bottom: 20px solid transparent;
-      border-right: 20px solid #E6E9EC;
+      border-left: 20px solid transparent;
       border-left-width: 0;
-      left: -1em;
-      content: "";
-      display: block;
-      top: 1.5em;
-      position: absolute;
-      width: 0; // screen-xs
       @media (max-width: $screen-xs) {
         top: 1em;
       }
@@ -30,9 +29,9 @@
   }
   %picture {
     float: left;
-    margin-top: 0.8em;
     width: 3em;
     height: 3em; // screen-xs
+    margin-top: 0.8em;
     @media (max-width: $screen-xs) {
       width: 2em;
       height: 2em;
@@ -58,7 +57,7 @@
     } // コメントセクション
     .page-comment-main {
       @extend %comment-section;
-      background: #E6E9EC;
+      background: #e6e9ec;
       border-radius: 0.35em;
     } // コメント本文
     .page-comment-body {
@@ -66,30 +65,30 @@
       word-wrap: break-word;
     }
     .page-comment-meta {
-      color: #E5ECF1;
-      font-size: .9em;
+      font-size: 0.9em;
+      color: #e5ecf1;
       text-align: right;
       * {
         vertical-align: 25%;
       }
     }
   } // show when hover
-  .page-comment-main:hover>.page-comment-control {
+  .page-comment-main:hover > .page-comment-control {
     display: block;
   } // display cheatsheet for comment form only
   .comment-form {
+    position: relative;
+    margin-top: 2em; // user icon
     border: none;
     .editor-cheatsheet {
       display: none;
     }
-    position: relative;
-    margin-top: 2em; // user icon
     .picture {
       @extend %picture;
     } // seciton
     .comment-form-main {
       @extend %comment-section;
-      background: #E6E9EC;
+      background: #e6e9ec;
       border-radius: 0.35em;
       .CodeMirror {
         border: 0px;
@@ -102,8 +101,8 @@
       height: 80px;
       &:focus,
       &:not(:invalid) {
-        transition: height 0.2s ease-out;
         height: 180px;
+        transition: height 0.2s ease-out;
       }
     }
     .CodeMirror {

+ 8 - 8
src/client/styles/scss/_create-page.scss

@@ -1,6 +1,6 @@
 .modal.create-page {
-
-  @media (min-width: $screen-sm-min) { // {{{ more than tablet size
+  // more than tablet size
+  @media (min-width: $screen-sm-min) {
     .modal-dialog {
       width: 750px;
     }
@@ -11,8 +11,8 @@
       margin-bottom: 10px;
     }
 
-    form, #template-form {
-
+    form,
+    #template-form {
       // layout
       .create-page-input-container {
         .create-page-input-row {
@@ -39,10 +39,10 @@
       }
       .page-today-input1 {
         width: 60px;
-        margin-left: 5px;
-        margin-right: 5px;
-        padding-left: 2px;
         padding-right: 2px;
+        padding-left: 2px;
+        margin-right: 5px;
+        margin-left: 5px;
       }
       .page-today-suffix {
       }
@@ -55,7 +55,7 @@
         flex: 1;
         input {
           min-width: 300px; // Workaround to display placeholder.
-                            //   cf https://github.com/ericgio/react-bootstrap-typeahead/issues/256
+          //   cf https://github.com/ericgio/react-bootstrap-typeahead/issues/256
         }
       }
 

+ 16 - 16
src/client/styles/scss/_editor-attachment.scss

@@ -1,7 +1,6 @@
 @import 'editor-overlay';
 
 .editor-container {
-
   // for Dropzone
   .dropzone {
     @mixin insertSimpleLineIcons($code) {
@@ -12,14 +11,15 @@
       }
     }
 
-    position: relative;   // against .overlay position: absolute
+    position: relative; // against .overlay position: absolute
 
     @include overlay-processing-style(overlay-dropzone-active, 2.5em, 0.5em);
 
     // unuploadable or rejected
-    &.dropzone-unuploadable, &.dropzone-rejected {
+    &.dropzone-unuploadable,
+    &.dropzone-rejected {
       .overlay.overlay-dropzone-active {
-        background: rgba(200,200,200,0.8);
+        background: rgba(200, 200, 200, 0.8);
 
         .overlay-content {
           color: #444;
@@ -36,9 +36,9 @@
       .overlay.overlay-dropzone-active {
         .overlay-content {
           // insert content
-          @include insertSimpleLineIcons("\e617");  // icon-exclamation
+          @include insertSimpleLineIcons('\e617'); // icon-exclamation
           &:after {
-            content: "File uploading is disabled";
+            content: 'File uploading is disabled';
           }
         }
       }
@@ -52,13 +52,13 @@
 
           .overlay-content {
             // insert content
-            @include insertSimpleLineIcons("\e084");  // icon-cloud-upload
+            @include insertSimpleLineIcons('\e084'); // icon-cloud-upload
             &:after {
-              content: "Drop here to upload";
+              content: 'Drop here to upload';
             }
             // style
             color: #666;
-            background: rgba(200,200,200,0.8);
+            background: rgba(200, 200, 200, 0.8);
           }
         }
       }
@@ -67,9 +67,9 @@
         .overlay.overlay-dropzone-active {
           .overlay-content {
             // insert content
-            @include insertSimpleLineIcons("\e032");  // icon-picture
+            @include insertSimpleLineIcons('\e032'); // icon-picture
             &:after {
-              content: "Only an image file is allowed";
+              content: 'Only an image file is allowed';
             }
           }
         }
@@ -79,9 +79,9 @@
         .overlay.overlay-dropzone-active {
           .overlay-content {
             // insert content
-            @include insertSimpleLineIcons("\e617");  // icon-exclamation
+            @include insertSimpleLineIcons('\e617'); // icon-exclamation
             &:after {
-              content: "Only 1 file is allowed";
+              content: 'Only 1 file is allowed';
             }
           }
         }
@@ -91,17 +91,16 @@
 
   .btn-open-dropzone {
     z-index: 2;
-    font-size: small;
     padding-top: 3px;
     padding-bottom: 3px;
+    font-size: small;
     border: none;
-    border-radius: 0;
     border-top: 1px dotted #ccc;
+    border-radius: 0;
     &:active {
       box-shadow: none;
     }
   }
-
 }
 
 #page-editor {
@@ -111,6 +110,7 @@
     }
   }
 }
+
 .comment-form {
   @media (max-width: $screen-sm) {
     .desc-long {

Некоторые файлы не были показаны из-за большого количества измененных файлов