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

Merge remote-tracking branch 'origin/master' into imprv/brushup-group-select-ui

Tatsuya Ise 8 лет назад
Родитель
Сommit
cc3e1bc928

+ 8 - 4
.eslintrc.js

@@ -37,8 +37,8 @@ module.exports = {
       2,
       {
         "SwitchCase": 1,
+        "ignoredNodes": ['JSXElement *', 'JSXElement'],
         "FunctionExpression": {"parameters": 2},
-        "CallExpression": {"parameters": 2}
       }
     ],
     "key-spacing": [
@@ -51,15 +51,19 @@ module.exports = {
       "error",
       "unix"
     ],
+    "no-unused-vars": [
+      "error",
+      { "args": "none" }
+    ],
     "quotes": [
       "error",
       "single"
     ],
-    "react/jsx-indent": [
+    "react/jsx-indent-props": [
       "error",
-      4,
-      { "ignoredNodes": ["JSXElement *"] }
+      2
     ],
+    "react/no-string-refs": 'off',
     "semi": [
       "error",
       "always"

+ 5 - 1
CHANGES.md

@@ -1,7 +1,11 @@
 CHANGES
 ========
 
-## 3.0.12-RC
+## 3.0.13-RC
+
+* Support: Translate /admin/security
+
+## 3.0.12
 
 * Feature: Support Vim/Emacs/Sublime-Text keybindings
 * Improvement: Add some CodeMirror themes (Eclipse, Dracula)

+ 2 - 9
lib/locales/en-US/translation.json

@@ -279,7 +279,6 @@
 		"Authentication mechanism settings": "Authentication mechanism settings",
     "note": "Note",
     "require_server_restart_change_auth": "Restarting the server is required if you switch the auth mechanism.",
-    "passport": "Passport",
     "auth_mechanism": "authentication mechanism",
     "recommended": "Recommended",
     "username_email_password": "Username, Email and Password authentication",
@@ -311,16 +310,12 @@
       "closed": "Invitation Only"
     },
     "configuration": "Configuration",
-    "default": "Default",
     "optional": "Optional",
     "ldap": {
-      "use_ldap": "Use LDAP",
-      "server_url": "Server URL",
       "server_url_detail": "The LDAP URL of the directory service in the format <code>ldap://host:port/DN</code> or <code>ldaps://host:port/DN</code>.",
       "bind_mode": "Binding Mode",
       "bind_manager": "Manager Bind",
       "bind_user": "User Bind",
-      "bind_DN": "Bind DN",
       "bind_DN_manager_detail": "The DN of the account that authenticates and queries the directory service",
       "bind_DN_user_detail1": "The query used to bind with the directory service.",
       "bind_DN_user_detail2": "Use <code>&#123;&#123;username&#125;&#125;</code> to reference the username entered in the login page.",
@@ -332,8 +327,6 @@
       "search_filter_detail2": "Use <code>&#123;&#123;username&#125;&#125;</code> to reference the username entered in the login page.",
       "search_filter_detail3": "If empty, the filter <code>(uid=&#123;&#123;username&#125;&#125;)</code> is used.",
       "search_filter_example": "Example to match with 'uid' or 'mail'",
-      "attribute_mapping": "Attribute Mapping",
-      "username": "Username",
       "username_detail": "Specification of mappings when creating new users",
       "Treat username matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>username</code> match",
   		"Treat username matching as identical_warn": "WARNING: Be aware of security because the system treats the same user as a match of <code>username</code>.",
@@ -343,8 +336,8 @@
       "group_search_filter_detail1": "The query used to filter for groups.",
       "group_search_filter_detail2": "Use <code>&#123;&#123;dn&#125;&#125;</code> to have it replaced of the found user object.",
       "group_search_filter_detail3": "<code>(&(cn=group1)(memberUid=&#123;&#123;dn&#125;&#125;))</code> hits the groups which has <code>cn=group1</code> and <code>memberUid</code> includes the user's <code>uid</code>(when <code>Group DN Property</code> is not changed from the default value.)",
-      "group_DN_property": "Group DN Property",
-      "group_DN_property_detail": "The property of user object to use in <code>&#123;&#123;dn&#125;&#125;</code> interpolation of <code>Group Search Filter</code>.",
+      "group_search_user_DN_property": "User DN Property",
+      "group_search_user_DN_property_detail": "The property of user object to use in <code>&#123;&#123;dn&#125;&#125;</code> interpolation of <code>Group Search Filter</code>.",
       "test_config": "Test Saved Configuration"
     },
     "Google OAuth": {

+ 26 - 35
lib/locales/ja/translation.json

@@ -293,25 +293,23 @@
     "users_without_account": "アカウントを持たないユーザーはアクセス不可",
     "example": "例",
     "restrict_emails": "登録可能なメールアドレスを制限することができます。",
-    "for_instance":"例えば、会社で使う場合 などと記載すると、",
-    "only_those":"その会社のメールアドレスを持っている人のみ登録可能になります。",
+    "for_instance":"例えば、",
+    "only_those":"と記載することで、そのドメインのメールアドレスを持っている人のみ登録可能になります。",
     "insert_single":"1行に1メールアドレス入力してください。",
     "Authentication mechanism settings":"認証機構設定",
     "note": "メモ",
     "require_server_restart_change_auth": "認証機構の変更後はサーバーを再起動してください。",
-    "passport": "パスポート",
     "auth_mechanism": "認証機構",
     "recommended": "推奨",
-    "username_email_password": "ユーサー名、Eメール、パスワードでの認証",
-    "ldap_auth": "LDAP認証",
-    "google_auth2": "Google OAuth2認証",
-    "facebook_auth2": "Facebook OAuth2認証",
-    "twitter_auth2": "Twitter OAuth認証",
-    "github_auth2": "Github OAuth2認証",
-    "crowi_auth": "Crowiクラシック認証機構",
+    "username_email_password": "ユーザー名、Eメール、パスワードでの認証",
+    "ldap_auth": "LDAP 認証",
+    "google_auth2": "Google OAuth2 認証",
+    "facebook_auth2": "Facebook OAuth2 認証",
+    "twitter_auth2": "Twitter OAuth 認証",
+    "github_auth2": "Github OAuth2 認証",
     "require_server_restart": "サーバーを再起動してください。",
-    "server_on_passport_auth": "パスポート認証でサーバーが稼働しています。",
-    "server_on_crowi_auth": "公式crowi認証でサーバーが稼働しています。",
+    "server_on_passport_auth": "Passport 認証機構でサーバーが稼働しています。",
+    "server_on_crowi_auth": "Crowi Classic 認証機構でサーバーが稼働しています。",
     "google_setting": "Google 設定",
     "connect_api_manager": "Google Cloud Platform の <a href=\"https://console.cloud.google.com/apis/credentials\">API Manager</a>から OAuth2 Client ID を作成すると、Google アカウントにコネクトして登録やログインが可能になります。",
     "access_api_manager": "<a href=\"https://console.cloud.google.com/apis/credentials\">API Manager</a> へアクセス",
@@ -331,41 +329,34 @@
       "closed": "非公開 (登録には管理者による招待が必要)"
     },
     "configuration": "コンフィギュレーション",
-    "default": "デフォルト",
-    "optional": "任意",
+    "optional": "オプション",
     "ldap": {
-      "use_ldap": "LDAPを使う",
-      "server_url": "サーバーURL",
-      "server_url_detail": "ディレクトリーサービスのLDAP URLを <code>ldap://host:port/DN</code> または <code>ldaps://host:port/DN</code>の形式で入力してください。",
-      "bind_mode": "モード",
+      "server_url_detail": "LDAP URLを <code>ldap://host:port/DN</code> または <code>ldaps://host:port/DN</code> の形式で入力してください。",
+      "bind_mode": "Bind モード",
       "bind_manager": "管理者 Bind",
       "bind_user": "ユーザー Bind",
-      "bind_DN": "DN Bind",
-      "bind_DN_manager_detail": "ディレクトリーサービスを認証、クエリするアカウントのDN",
-      "bind_DN_user_detail1": "ディレクトリーサービスをBindするのに用いるクエリ",
-      "bind_DN_user_detail2": "ログイン時のユーザー名を使用するには <code>&#123;&#123;username&#125;&#125;</code> の形式を使用してください。",
+      "bind_DN_manager_detail": "ディレクトリーサービスに認証する際のアカウント DN",
+      "bind_DN_user_detail1": "ディレクトリーサービスに Bind するアカウント DN を決定するためのクエリ",
+      "bind_DN_user_detail2": "ログイン時に入力されるユーザー名を使用するには <code>&#123;&#123;username&#125;&#125;</code> の形式を使用してください。",
       "bind_DN_password": "Bind DN パスワード",
       "bind_DN_password_manager_detail": "Bind DN アカウントのパスワード",
-      "bind_DN_password_user_detail": "ログイン時のパスワードがBindに使用されます。",
+      "bind_DN_password_user_detail": "ログイン時のパスワードが使用されます。",
       "search_filter": "検索フィルター",
-      "search_filter_detail1": "認証されたユーザーを特定するのに用いるクエリ",
-      "search_filter_detail2": "ログイン時のユーザー名を使用するには<code>&#123;&#123;username&#125;&#125;</code> の形式を使用してください。",
+      "search_filter_detail1": "認証されるユーザーを一意に決定するための LDAP フィルタ",
+      "search_filter_detail2": "ログイン時のユーザー名を使用するには <code>&#123;&#123;username&#125;&#125;</code> の形式を使用してください。",
       "search_filter_detail3": "空欄の場合 <code>(uid=&#123;&#123;username&#125;&#125;)</code> が使用されます。",
-      "search_filter_example": "例えば 'uid' または 'mail' が一致するユーザーを探す場合",
-      "attribute_mapping": "要素の関連付け",
-      "username": "ユーザー名",
+      "search_filter_example": "'uid' または 'mail' に一致させる場合の例",
       "username_detail": "新規ユーザーの関連付けを設定",
       "Treat username matching as identical": "新規ログイン時、<code>username</code> が一致したローカルアカウントが存在した場合は自動的に紐付ける",
       "Treat username matching as identical_warn": "WARNING: <code>username</code> の一致を以て同一ユーザーであるとみなすので、セキュリティに注意してください",
-      "group_search_base_DN": "グループ検索ベースDN",
-      "group_search_base_DN_detail": "グループ検索を実行するベースDN。利用するのであれば <code>Group Search Filter</code> を定義する必要があります。",
+      "group_search_base_DN": "グループ検索ベース DN",
+      "group_search_base_DN_detail": "グループ検索を実行するベース DN。利用する場合は <code>グループ検索フィルター</code> も入力する必要があります。",
       "group_search_filter": "グループ検索フィルター",
       "group_search_filter_detail1": "グループフィルターに用いるクエリ",
-      "group_search_filter_detail2": "ユーザーオブジェクトを使用する場合は <code>&#123;&#123;dn&#125;&#125;</code> を用いてください。",
-      "group_search_filter_detail3": "<code>(&(cn=group1)(memberUid=&#123;&#123;dn&#125;&#125;))</code> は <code>cn=group1</code> を持ち <code>memberUid</code> が <code>uid</code> を含むグループが検索されます(<code>Group DN Property</code> がデフォルトから変更されていない場合)",
-      "group_DN_property": "グループDNプロパティー",
-      "group_DN_property_detail": "<code>Group Search Filter</code> の <code>&#123;&#123;dn&#125;&#125;</code> インターポレーションで使用するユーザーオブジェクトのプロパティー",
-      "test_config": "新しい設定を試す"
+      "group_search_filter_detail2": "ログイン対象ユーザーオブジェクトのプロパティーで置換する場合は <code>&#123;&#123;dn&#125;&#125;</code> を用いてください。",
+      "group_search_filter_detail3": "<code>(&(cn=group1)(memberUid=&#123;&#123;dn&#125;&#125;))</code> は <code>cn=group1</code> と、ユーザーの <code>uid</code> を含む <code>memberUid</code> を持つグループにヒットします(<code>ユーザーの DN プロパティー</code> がデフォルトから変更されていない場合)",
+      "group_search_user_DN_property": "ユーザーの DN プロパティー",
+      "group_search_user_DN_property_detail": "<code>グループ検索フィルター</code> 内の <code>&#123;&#123;dn&#125;&#125;</code> で置換される、ユーザーオブジェクトのプロパティー"
     },
     "Google OAuth": {
     },

+ 3 - 3
lib/views/admin/security.html

@@ -85,7 +85,7 @@
             <label for="settingForm[security:registrationWhiteList]" class="col-xs-3 control-label">{{ t('The whitelist of registration permission E-mail address') }}</label>
             <div class="col-xs-8">
               <textarea class="form-control" type="textarea" name="settingForm[security:registrationWhiteList]" placeholder="{{ t('security_setting.example') }}: @crowi.wiki">{{ settingForm['security:registrationWhiteList']|join('&#13')|raw }}</textarea>
-              <p class="help-block">{{ t("security_setting.restrict_emails") }}{{ t("security_setting.for_instance") }}<code>@crowi.wiki</code>{{ t("security_setting.only_those") }}<br>
+              <p class="help-block">{{ t("security_setting.restrict_emails") }}{{ t("security_setting.for_instance") }}<code>@growi.org</code>{{ t("security_setting.only_those") }}<br>
               {{ t("security_setting.insert_single") }}</p>
             </div>
           </div>
@@ -112,7 +112,7 @@
                       {% if true === settingForm['security:isEnabledPassport'] %}checked="checked"{% endif %}>
                   <label for="radioPassportAuthMech">
                     <a href="http://passportjs.org/">
-                      <img src="/images/admin/security/passport-logo.svg" class="passport-logo"> {{ t("security_setting.passport") }}
+                      <img src="/images/admin/security/passport-logo.svg" class="passport-logo"> Passport
                     </a> {{ t("security_setting.auth_mechanism") }} <small class="text-success">({{ t("security_setting.recommended") }})</small>
                   </label>
                 </div>
@@ -132,7 +132,7 @@
                   <input type="radio" id="radioCrowiAuthMech" name="settingForm[security:isEnabledPassport]" value="false"
                       {% if !settingForm['security:isEnabledPassport'] %}checked="checked"{% endif %}>
                   <label for="radioCrowiAuthMech">
-                    {{ t("security_setting.crowi_auth") }}
+                    Crowi Classic {{ t("security_setting.auth_mechanism") }}
                   </label>
                 </div>
               </h4>

+ 26 - 15
lib/views/admin/widget/passport/ldap.html

@@ -6,7 +6,7 @@
     {% set nameForIsLdapEnabled = "settingForm[security:passport-ldap:isEnabled]" %}
     {% set isLdapEnabled = settingForm['security:passport-ldap:isEnabled'] %}
     <div class="form-group">
-      <label for="{{nameForIsLdapEnabled}}" class="col-xs-3 control-label">{{ t("security_setting.ldap.use_ldap") }}</label>
+      <label for="{{nameForIsLdapEnabled}}" class="col-xs-3 control-label">Use LDAP</label>
       <div class="col-xs-6">
         <div class="btn-group btn-toggle" data-toggle="buttons">
           <label class="btn btn-default btn-rounded btn-outline {% if isLdapEnabled %}active{% endif %}" data-active-class="primary">
@@ -24,7 +24,7 @@
     <div class="passport-ldap-hide-when-disabled" {%if !isLdapEnabled %}style="display: none;"{% endif %}>
 
       <div class="form-group">
-        <label for="settingForm[security:passport-ldap:serverUrl]" class="col-xs-3 control-label">{{ t("security_setting.ldap.server_url") }}</label>
+        <label for="settingForm[security:passport-ldap:serverUrl]" class="col-xs-3 control-label">Server URL</label>
         <div class="col-xs-6">
           <input class="form-control" type="text"
               name="settingForm[security:passport-ldap:serverUrl]" value="{{ settingForm['security:passport-ldap:serverUrl'] || '' }}">
@@ -56,7 +56,7 @@
       </div>
 
       <div class="form-group">
-        <label for="settingForm[security:passport-ldap:bindDN]" class="col-xs-3 control-label">{{ t("security_setting.ldap.bind_DN") }}</label>
+        <label for="settingForm[security:passport-ldap:bindDN]" class="col-xs-3 control-label">Bind DN</label>
         <div class="col-xs-6">
           <input class="form-control" type="text"
               name="settingForm[security:passport-ldap:bindDN]" value="{{ settingForm['security:passport-ldap:bindDN'] || '' }}">
@@ -96,26 +96,29 @@
       <div class="form-group">
         <label for="settingForm[security:passport-ldap:searchFilter]" class="col-xs-3 control-label">{{ t("security_setting.ldap.search_filter") }}</label>
         <div class="col-xs-6">
-          <input class="form-control" type="text" placeholder="{{ t("security_setting.default") }}: (uid={% raw %}{{username}}{% endraw %})"
+          <input class="form-control" type="text" placeholder="Default: (uid={% raw %}{{username}}{% endraw %})"
               name="settingForm[security:passport-ldap:searchFilter]" value="{{ settingForm['security:passport-ldap:searchFilter'] || '' }}">
           <p class="help-block">
             <small>
               {{ t("security_setting.ldap.search_filter_detail1") }}<br>
               {{ t("security_setting.ldap.search_filter_detail2") }}<br>
-              {{ t("security_setting.ldap.search_filter_detail3") }}<br>
-              <br>
+              {{ t("security_setting.ldap.search_filter_detail3") }}
+            </small>
+          </p>
+          <p>
+            <small>
               {{ t("security_setting.ldap.search_filter_example") }}: <code>(|(uid={% raw %}{{username}}{% endraw %})(mail={% raw %}{{username}}{% endraw %}))</code>
             </small>
           </p>
         </div>
       </div>
 
-      <h4>{{ t("security_setting.ldap.attribute_mapping") }} ({{ t("security_setting.optional") }})</h4>
+      <h4>Attribute Mapping ({{ t("security_setting.optional") }})</h4>
 
       <div class="form-group">
-        <label for="settingForm[security:passport-ldap:attrMapUsername]" class="col-xs-3 control-label">{{ t("security_setting.ldap.username") }}</label>
+        <label for="settingForm[security:passport-ldap:attrMapUsername]" class="col-xs-3 control-label">username</label>
         <div class="col-xs-6">
-          <input class="form-control" type="text" placeholder="{{ t("security_setting.default") }}: uid"
+          <input class="form-control" type="text" placeholder="Default: uid"
               name="settingForm[security:passport-ldap:attrMapUsername]" value="{{ settingForm['security:passport-ldap:attrMapUsername'] || '' }}">
           <p class="help-block">
             <small>
@@ -129,6 +132,11 @@
             <label for="cbSameUsernameTreatedAsIdenticalUser">
               {{ t("security_setting.ldap.Treat username matching as identical") }}
             </label>
+            <p class="help-block">
+              <small>
+                {{ t("security_setting.ldap.Treat username matching as identical_warn") }}
+              </small>
+            </p>
           </div>
         </div>
       </div>
@@ -144,7 +152,7 @@
           <p class="help-block">
             <small>
               {{ t("security_setting.ldap.group_search_base_DN_detail") }}<br>
-              {{ t("security_setting.example") }}: <code>ou=groups,dc=domain,dc=com</code><br>
+              {{ t("security_setting.example") }}: <code>ou=groups,dc=domain,dc=com</code>
             </small>
           </p>
         </div>
@@ -158,8 +166,11 @@
           <p class="help-block">
             <small>
               {{ t("security_setting.ldap.group_search_filter_detail1") }}<br>
-              {{ t("security_setting.ldap.group_search_filter_detail2") }}<br>
-              <br>
+              {{ t("security_setting.ldap.group_search_filter_detail2") }}
+            </small>
+          </p>
+          <p class="help-block">
+            <small>
               {{ t("security_setting.example") }}: {{ t("security_setting.ldap.group_search_filter_detail3") }}
             </small>
           </p>
@@ -167,13 +178,13 @@
       </div>
 
       <div class="form-group">
-        <label for="settingForm[security:passport-ldap:groupSearchFilter]" class="col-xs-3 control-label">{{ t("security_setting.ldap.group_DN_property") }}</label>
+        <label for="settingForm[security:passport-ldap:groupSearchFilter]" class="col-xs-3 control-label">{{ t("security_setting.ldap.group_search_user_DN_property") }}</label>
         <div class="col-xs-6">
-          <input class="form-control" type="text" placeholder="{{ t("security_setting.default") }}: uid"
+          <input class="form-control" type="text" placeholder="Default: uid"
               name="settingForm[security:passport-ldap:groupDnProperty]" value="{{ settingForm['security:passport-ldap:groupDnProperty'] || '' }}">
           <p class="help-block">
             <small>
-              {{ t("security_setting.ldap.group_DN_property_detail") }}
+              {{ t("security_setting.ldap.group_search_user_DN_property_detail") }}
             </small>
           </p>
         </div>

+ 1 - 1
package.json

@@ -161,7 +161,7 @@
     "colors": "^1.1.2",
     "commander": "^2.11.0",
     "connect-browser-sync": "^2.1.0",
-    "eslint": "^4.18.2",
+    "eslint": "^4.19.1",
     "eslint-plugin-react": "^7.7.0",
     "mocha": "^5.0.0",
     "morgan": "^1.8.2",

BIN
public/images/icons/emacs.png


BIN
public/images/icons/sublime.png


BIN
public/images/icons/vim.png


+ 47 - 20
resource/js/components/PageEditor/Editor.js

@@ -54,6 +54,7 @@ export default class Editor extends React.Component {
       value: this.props.value,
       dropzoneActive: false,
       isUploading: false,
+      isLoadingKeymap: false,
     };
 
     this.loadedThemeSet = new Set(['eclipse', 'elegant']);   // themes imported in _vendor.scss
@@ -78,7 +79,9 @@ export default class Editor extends React.Component {
 
     this.getDropzoneAccept = this.getDropzoneAccept.bind(this);
     this.getDropzoneClassName = this.getDropzoneClassName.bind(this);
-    this.renderOverlay = this.renderOverlay.bind(this);
+    this.renderDropzoneOverlay = this.renderDropzoneOverlay.bind(this);
+
+    this.renderLoadingKeymapOverlay = this.renderLoadingKeymapOverlay.bind(this);
   }
 
 
@@ -98,7 +101,6 @@ export default class Editor extends React.Component {
     this.loadTheme(theme);
 
     // set keymap
-    const prevKeymapMode = this.props.editorOptions.keymapMode;
     const keymapMode = nextProps.editorOptions.keymapMode;
     this.setKeymapMode(keymapMode);
   }
@@ -163,8 +165,6 @@ export default class Editor extends React.Component {
    * @param {string} theme
    */
   loadTheme(theme) {
-    // load theme
-    let cssList = [];
     if (!this.loadedThemeSet.has(theme)) {
       this.loadCss(urljoin(this.cmCdnRoot, `theme/${theme}.min.css`));
 
@@ -194,7 +194,13 @@ export default class Editor extends React.Component {
       this.loadedKeymapSet.add(keymapMode);
     }
 
-    return Promise.all(scriptList.concat(cssList));
+    // set loading state
+    this.setState({ isLoadingKeymap: true });
+
+    return Promise.all(scriptList.concat(cssList))
+      .then(() => {
+        this.setState({ isLoadingKeymap: false });
+      });
   }
 
   /**
@@ -211,9 +217,9 @@ export default class Editor extends React.Component {
     }
 
     this.loadKeymapMode(keymapMode)
-    .then(() => {
-      this.getCodeMirror().setOption('keyMap', keymapMode);
-    });
+      .then(() => {
+        this.getCodeMirror().setOption('keyMap', keymapMode);
+      });
   }
 
   /**
@@ -345,7 +351,7 @@ export default class Editor extends React.Component {
     let accept = 'null';    // reject all
     if (this.props.isUploadable) {
       if (!this.props.isUploadableFile) {
-        accept = 'image/*'  // image only
+        accept = 'image/*'; // image only
       }
       else {
         accept = '';        // allow all
@@ -376,8 +382,8 @@ export default class Editor extends React.Component {
     return className;
   }
 
-  renderOverlay() {
-    const overlayStyle = {
+  getOverlayStyle() {
+    return {
       position: 'absolute',
       zIndex: 4,  // forward than .CodeMirror-gutters
       top: 0,
@@ -385,20 +391,36 @@ export default class Editor extends React.Component {
       bottom: 0,
       left: 0,
     };
+  }
+
+  renderDropzoneOverlay() {
+    const overlayStyle = this.getOverlayStyle();
 
     return (
-      <div style={overlayStyle} className="dropzone-overlay">
+      <div style={overlayStyle} className="overlay">
         {this.state.isUploading &&
-          <span className="dropzone-overlay-content">
-            <i className="fa fa-spinner fa-pulse fa-fw"></i>
+          <span className="overlay-content">
+            <div className="speeding-wheel d-inline-block"></div>
             <span className="sr-only">Uploading...</span>
           </span>
         }
-        {!this.state.isUploading && <span className="dropzone-overlay-content"></span>}
+        {!this.state.isUploading && <span className="overlay-content"></span>}
       </div>
     );
   }
 
+  renderLoadingKeymapOverlay() {
+    const overlayStyle = this.getOverlayStyle();
+
+    return this.state.isLoadingKeymap
+      ? <div style={overlayStyle} className="loading-keymap overlay">
+          <span className="overlay-content">
+            <div className="speeding-wheel d-inline-block"></div> Loading Keymap ...
+          </span>
+        </div>
+      : '';
+  }
+
   render() {
     const flexContainer = {
       height: '100%',
@@ -408,7 +430,7 @@ export default class Editor extends React.Component {
 
     const theme = this.props.editorOptions.theme || 'elegant';
     const styleActiveLine = this.props.editorOptions.styleActiveLine || undefined;
-    return (
+    return <React.Fragment>
       <div style={flexContainer}>
         <Dropzone
           ref="dropzone"
@@ -422,7 +444,7 @@ export default class Editor extends React.Component {
           onDragLeave={this.onDragLeave}
           onDrop={this.onDrop}
         >
-          { this.state.dropzoneActive && this.renderOverlay() }
+          { this.state.dropzoneActive && this.renderDropzoneOverlay() }
 
           <ReactCodeMirror
             ref="cm"
@@ -456,7 +478,7 @@ export default class Editor extends React.Component {
                 'Enter': this.handleEnterKey,
                 'Tab': 'indentMore',
                 'Shift-Tab': 'indentLess',
-                'Ctrl-Q': (cm) => { cm.foldCode(cm.getCursor()) },
+                'Ctrl-Q': (cm) => { cm.foldCode(cm.getCursor()); },
               }
             }}
             onScroll={(editor, data) => {
@@ -480,15 +502,19 @@ export default class Editor extends React.Component {
         </Dropzone>
 
         <button type="button" className="btn btn-default btn-block btn-open-dropzone"
-            onClick={() => {this.refs.dropzone.open()}}>
+          onClick={() => {this.refs.dropzone.open();}}>
 
           <i className="icon-paper-clip" aria-hidden="true"></i>&nbsp;
           Attach files by dragging &amp; dropping,&nbsp;
           <span className="btn-link">selecting them</span>,&nbsp;
           or pasting from the clipboard.
         </button>
+
+        { this.renderLoadingKeymapOverlay() }
+
       </div>
-    );
+
+    </React.Fragment>;
   }
 
 }
@@ -496,6 +522,7 @@ export default class Editor extends React.Component {
 Editor.propTypes = {
   value: PropTypes.string,
   options: PropTypes.object,
+  editorOptions: PropTypes.object,
   isUploadable: PropTypes.bool,
   isUploadableFile: PropTypes.bool,
   onChange: PropTypes.func,

+ 7 - 2
resource/js/components/PageEditor/OptionsSelector.js

@@ -142,14 +142,19 @@ export default class OptionsSelector extends React.Component {
     const optionElems = [];
     for (let mode in this.keymapModes) {
       const label = this.keymapModes[mode];
-      optionElems.push(<option key={mode} value={mode}>{label}</option>);
+      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>
+      );
     }
 
     const bsClassName = 'form-control-dummy'; // set form-control* to shrink width
 
     return (
       <FormGroup controlId="formControlsSelect">
-        <ControlLabel>Mode:</ControlLabel>
+        <ControlLabel>Keymap:</ControlLabel>
         <FormControl componentClass="select" placeholder="select" bsClass={bsClassName} className="btn-group-sm selectpicker"
             onChange={this.onChangeKeymapMode}
             inputRef={ el => this.keymapModeSelectorInputEl=el }>

+ 45 - 37
resource/styles/scss/_on-edit.scss

@@ -82,7 +82,6 @@ body.on-edit {
                          + 42px                     // .nav height
                          + 1px                      // .page-editor-footer border-top
                          + 40px;                    // .page-editor-footer min-height
-      $preview-margin: $header-plus-footer;
       $editor-margin: $header-plus-footer + 22px;   // .btn-open-dropzone height
 
       #page-editor {
@@ -91,14 +90,16 @@ body.on-edit {
         .row,
         .page-editor-preview-container,
         .page-editor-preview-body {
-          height: calc(100vh - #{$preview-margin});
+          height: calc(100vh - #{$header-plus-footer});
         }
         // left(editor)
-        .page-editor-editor-container,
+        .page-editor-editor-container {
+          height: calc(100vh - #{$header-plus-footer});
         .react-codemirror2, .CodeMirror, .CodeMirror-scroll {
           height: calc(100vh - #{$editor-margin});
         }
       }
+      }
 
       .page-editor-footer {
         width: 100%;
@@ -184,9 +185,33 @@ body.on-edit {
       }
     }
 
+    .overlay {
+      // layout
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      // style
+      margin: 0 15px;
+    }
+    .overlay-content {
+      font-size: 2.5em;
+      padding: 0.5em;
+    }
+
+    @mixin overlay-processing-style() {
+      .overlay {
+        background: rgba(255,255,255,0.5);
+      }
+      .overlay-content {
+        padding: 0.3em;
+        background: rgba(200,200,200,0.5);
+        color: #444;
+      }
+    }
+
     // for Dropzone
     .dropzone {
-      @mixin insertFontAwesome($code) {
+      @mixin insertSimpleLineIcons($code) {
         &:before {
           margin-right: 0.2em;
           font-family: 'simple-line-icons';
@@ -194,46 +219,25 @@ body.on-edit {
         }
       }
 
-      // default layout and style
-      .dropzone-overlay {
-        // layout
-        display: flex;
-        justify-content: center;
-        align-items: center;
-        // style
-        margin: 0 15px;
-      }
-      .dropzone-overlay-content {
-        font-size: 2.5em;
-        padding: 0.5em;
-      }
-
       // unuploadable or rejected
       &.dropzone-unuploadable, &.dropzone-rejected {
-        .dropzone-overlay {
+        .overlay {
           background: rgba(200,200,200,0.8);
         }
-        .dropzone-overlay-content {
+        .overlay-content {
           color: #444;
         }
       }
       // uploading
       &.dropzone-uploading {
-        .dropzone-overlay {
-          background: rgba(255,255,255,0.5);
-        }
-        .dropzone-overlay-content {
-          padding: 0.3em;
-          background: rgba(200,200,200,0.5);
-          color: #444;
-        }
+        @include overlay-processing-style();
       }
 
       // unuploadable
       &.dropzone-unuploadable {
-        .dropzone-overlay-content {
+        .overlay-content {
           // insert content
-          @include insertFontAwesome("\e617");  // icon-exclamation
+          @include insertSimpleLineIcons("\e617");  // icon-exclamation
           &:after {
             content: "File uploading is disabled";
           }
@@ -243,12 +247,12 @@ body.on-edit {
       &.dropzone-uploadable {
         // accepted
         &.dropzone-accepted:not(.dropzone-rejected) {
-          .dropzone-overlay {
+          .overlay {
             border: 4px dashed #ccc;
           }
-          .dropzone-overlay-content {
+          .overlay-content {
             // insert content
-            @include insertFontAwesome("\e084");  // icon-cloud-upload
+            @include insertSimpleLineIcons("\e084");  // icon-cloud-upload
             &:after {
               content: "Drop here to upload";
             }
@@ -258,17 +262,17 @@ body.on-edit {
           }
         }
         // file type mismatch
-        &.dropzone-rejected:not(.dropzone-uploadablefile) .dropzone-overlay-content {
+        &.dropzone-rejected:not(.dropzone-uploadablefile) .overlay-content {
           // insert content
-          @include insertFontAwesome("\e032");  // icon-picture
+          @include insertSimpleLineIcons("\e032");  // icon-picture
           &:after {
             content: "Only an image file is allowed";
           }
         }
         // multiple files
-        &.dropzone-accepted.dropzone-rejected .dropzone-overlay-content {
+        &.dropzone-accepted.dropzone-rejected .overlay-content {
           // insert content
-          @include insertFontAwesome("\e617");  // icon-exclamation
+          @include insertSimpleLineIcons("\e617");  // icon-exclamation
           &:after {
             content: "Only 1 file is allowed";
           }
@@ -276,6 +280,10 @@ body.on-edit {
       }
     } // end of.dropzone
 
+    .loading-keymap {
+      @include overlay-processing-style();
+    }
+
     .btn-open-dropzone {
       z-index: 2;
       font-size: small;

+ 10 - 5
yarn.lock

@@ -2528,9 +2528,9 @@ eslint-visitor-keys@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
 
-eslint@^4.18.2:
-  version "4.18.2"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.18.2.tgz#0f81267ad1012e7d2051e186a9004cc2267b8d45"
+eslint@^4.19.1:
+  version "4.19.1"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.19.1.tgz#32d1d653e1d90408854bfb296f076ec7e186a300"
   dependencies:
     ajv "^5.3.0"
     babel-code-frame "^6.22.0"
@@ -2541,7 +2541,7 @@ eslint@^4.18.2:
     doctrine "^2.1.0"
     eslint-scope "^3.7.1"
     eslint-visitor-keys "^1.0.0"
-    espree "^3.5.2"
+    espree "^3.5.4"
     esquery "^1.0.0"
     esutils "^2.0.2"
     file-entry-cache "^2.0.0"
@@ -2563,6 +2563,7 @@ eslint@^4.18.2:
     path-is-inside "^1.0.2"
     pluralize "^7.0.0"
     progress "^2.0.0"
+    regexpp "^1.0.1"
     require-uncached "^1.0.3"
     semver "^5.3.0"
     strip-ansi "^4.0.0"
@@ -2570,7 +2571,7 @@ eslint@^4.18.2:
     table "4.0.2"
     text-table "~0.2.0"
 
-espree@^3.5.2:
+espree@^3.5.4:
   version "3.5.4"
   resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7"
   dependencies:
@@ -6207,6 +6208,10 @@ regexp-clone@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/regexp-clone/-/regexp-clone-0.0.1.tgz#a7c2e09891fdbf38fbb10d376fb73003e68ac589"
 
+regexpp@^1.0.1:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-1.1.0.tgz#0e3516dd0b7904f413d2d4193dce4618c3a689ab"
+
 regexpu-core@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b"