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

Merge branch 'master' into imprv/enable-search-help

Yuki Takei 7 лет назад
Родитель
Сommit
1c233cb3e5

+ 1 - 1
CHANGES.md

@@ -10,7 +10,7 @@ CHANGES
 
 
 ## 3.1.13-RC
 ## 3.1.13-RC
 
 
-* 
+* Improvement: Add attribute mappings for email to LDAP settings
 
 
 ## 3.1.12
 ## 3.1.12
 
 

+ 1 - 0
lib/form/admin/securityPassportLdap.js

@@ -17,6 +17,7 @@ module.exports = form(
   field('settingForm[security:passport-ldap:searchFilter]'),
   field('settingForm[security:passport-ldap:searchFilter]'),
   field('settingForm[security:passport-ldap:attrMapUsername]'),
   field('settingForm[security:passport-ldap:attrMapUsername]'),
   field('settingForm[security:passport-ldap:attrMapName]'),
   field('settingForm[security:passport-ldap:attrMapName]'),
+  field('settingForm[security:passport-ldap:attrMapMail]'),
   field('settingForm[security:passport-ldap:isSameUsernameTreatedAsIdenticalUser]').trim().toBooleanStrict(),
   field('settingForm[security:passport-ldap:isSameUsernameTreatedAsIdenticalUser]').trim().toBooleanStrict(),
   field('settingForm[security:passport-ldap:groupSearchBase]'),
   field('settingForm[security:passport-ldap:groupSearchBase]'),
   field('settingForm[security:passport-ldap:groupSearchFilter]'),
   field('settingForm[security:passport-ldap:groupSearchFilter]'),

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

@@ -362,7 +362,8 @@
       "search_filter_example1": "Match with 'uid' or 'mail'",
       "search_filter_example1": "Match with 'uid' or 'mail'",
       "search_filter_example2": "Match with 'sAMAccountName' for Active Directory",
       "search_filter_example2": "Match with 'sAMAccountName' for Active Directory",
       "username_detail": "Specification of mappings for <code>username</code> when creating new users",
       "username_detail": "Specification of mappings for <code>username</code> when creating new users",
-      "name_detail": "Specification of mappings for <code>name</code> when creating new users",
+      "name_detail": "Specification of mappings for full name when creating new users",
+      "mail_detail": "Specification of mappings for mail address when creating new users",
       "group_search_base_DN": "Group Search Base DN",
       "group_search_base_DN": "Group Search Base DN",
       "group_search_base_DN_detail": "The base DN from which to search for groups. If defined, also <code>Group Search Filter</code> must be defined for the search to work.",
       "group_search_base_DN_detail": "The base DN from which to search for groups. If defined, also <code>Group Search Filter</code> must be defined for the search to work.",
       "group_search_filter": "Group Search Filter",
       "group_search_filter": "Group Search Filter",

+ 2 - 1
lib/locales/ja/translation.json

@@ -379,7 +379,8 @@
       "search_filter_example1": "'uid' または 'mail' に一致",
       "search_filter_example1": "'uid' または 'mail' に一致",
       "search_filter_example2": "'sAMAccountName' に一致 (Active Directory)",
       "search_filter_example2": "'sAMAccountName' に一致 (Active Directory)",
       "username_detail": "新規ユーザーのアカウント名(<code>username</code>)に関連付ける属性",
       "username_detail": "新規ユーザーのアカウント名(<code>username</code>)に関連付ける属性",
-      "name_detail": "新規ユーザーの表示名(<code>name</code>)に関連付ける属性",
+      "name_detail": "新規ユーザーの表示名に関連付ける属性",
+      "mail_detail": "新規ユーザーのメールアドレスに関連付ける属性",
       "group_search_base_DN": "グループ検索ベース DN",
       "group_search_base_DN": "グループ検索ベース DN",
       "group_search_base_DN_detail": "グループ検索を実行するベース DN。利用する場合は <code>グループ検索フィルター</code> も入力する必要があります。",
       "group_search_base_DN_detail": "グループ検索を実行するベース DN。利用する場合は <code>グループ検索フィルター</code> も入力する必要があります。",
       "group_search_filter": "グループ検索フィルター",
       "group_search_filter": "グループ検索フィルター",

+ 1 - 0
lib/models/config.js

@@ -61,6 +61,7 @@ module.exports = function(crowi) {
       'security:passport-ldap:searchFilter' : undefined,
       'security:passport-ldap:searchFilter' : undefined,
       'security:passport-ldap:attrMapUsername' : undefined,
       'security:passport-ldap:attrMapUsername' : undefined,
       'security:passport-ldap:attrMapName' : undefined,
       'security:passport-ldap:attrMapName' : undefined,
+      'security:passport-ldap:attrMapMail' : undefined,
       'security:passport-ldap:groupSearchBase' : undefined,
       'security:passport-ldap:groupSearchBase' : undefined,
       'security:passport-ldap:groupSearchFilter' : undefined,
       'security:passport-ldap:groupSearchFilter' : undefined,
       'security:passport-ldap:groupDnProperty' : undefined,
       'security:passport-ldap:groupDnProperty' : undefined,

+ 4 - 2
lib/models/external-account.js

@@ -65,10 +65,12 @@ class ExternalAccount {
    * @param {string} providerType
    * @param {string} providerType
    * @param {string} accountId
    * @param {string} accountId
    * @param {object} usernameToBeRegistered the username of User entity that will be created when accountId is not found
    * @param {object} usernameToBeRegistered the username of User entity that will be created when accountId is not found
+   * @param {object} nameToBeRegistered the name of User entity that will be created when accountId is not found
+   * @param {object} mailToBeRegistered the mail of User entity that will be created when accountId is not found
    * @returns {Promise<ExternalAccount>}
    * @returns {Promise<ExternalAccount>}
    * @memberof ExternalAccount
    * @memberof ExternalAccount
    */
    */
-  static findOrRegister(providerType, accountId, usernameToBeRegistered, nameToBeRegistered) {
+  static findOrRegister(providerType, accountId, usernameToBeRegistered, nameToBeRegistered, mailToBeRegistered) {
 
 
     return this.findOne({ providerType, accountId })
     return this.findOne({ providerType, accountId })
       .then(account => {
       .then(account => {
@@ -92,7 +94,7 @@ class ExternalAccount {
 
 
             // create a new User with STATUS_ACTIVE
             // create a new User with STATUS_ACTIVE
             debug(`ExternalAccount '${accountId}' is not found, it is going to be registered.`);
             debug(`ExternalAccount '${accountId}' is not found, it is going to be registered.`);
-            return User.createUser(nameToBeRegistered, usernameToBeRegistered, undefined, undefined, undefined, User.STATUS_ACTIVE);
+            return User.createUser(nameToBeRegistered, usernameToBeRegistered, mailToBeRegistered, undefined, undefined, User.STATUS_ACTIVE);
           })
           })
           .then(newUser => {
           .then(newUser => {
             return this.associate(providerType, accountId, newUser);
             return this.associate(providerType, accountId, newUser);

+ 10 - 5
lib/models/user.js

@@ -680,13 +680,19 @@ module.exports = function(crowi) {
     );
     );
   };
   };
 
 
-  userSchema.statics.createUserByEmailAndPasswordAndStatus = function(name, username, email, password, lang, status, callback) {
-    var User = this
+  userSchema.statics.createUserByEmailAndPasswordAndStatus = async function(name, username, email, password, lang, status, callback) {
+    const User = this
       , newUser = new User();
       , newUser = new User();
 
 
+    // check email duplication because email must be unique
+    const count = await this.count({ email });
+    if (count > 0) {
+      email = generateRandomEmail();
+    }
+
     newUser.name = name;
     newUser.name = name;
     newUser.username = username;
     newUser.username = username;
-    newUser.email = email || generateRandomEmail();   // don't set undefined for backward compatibility -- 2017.12.27 Yuki Takei
+    newUser.email = email;
     if (password != null) {
     if (password != null) {
       newUser.setPassword(password);
       newUser.setPassword(password);
     }
     }
@@ -710,9 +716,8 @@ module.exports = function(crowi) {
   };
   };
 
 
   /**
   /**
-   * A wrapper function of createUserByEmailAndPasswordAndStatus
+   * A wrapper function of createUserByEmailAndPasswordAndStatus with callback
    *
    *
-   * @return {Promise<User>}
    */
    */
   userSchema.statics.createUserByEmailAndPassword = function(name, username, email, password, lang, callback) {
   userSchema.statics.createUserByEmailAndPassword = function(name, username, email, password, lang, callback) {
     this.createUserByEmailAndPasswordAndStatus(name, username, email, password, lang, undefined, callback);
     this.createUserByEmailAndPasswordAndStatus(name, username, email, password, lang, undefined, callback);

+ 6 - 1
lib/routes/hackmd.js

@@ -12,7 +12,7 @@ module.exports = function(crowi, app) {
   const manifest = require(path.join(crowi.publicDir, 'manifest.json'));
   const manifest = require(path.join(crowi.publicDir, 'manifest.json'));
   const agentScriptPath = path.join(crowi.publicDir, manifest['js/agent-for-hackmd.js']);
   const agentScriptPath = path.join(crowi.publicDir, manifest['js/agent-for-hackmd.js']);
   // generate swig template
   // generate swig template
-  const agentScriptContentTpl = swig.compileFile(agentScriptPath);
+  let agentScriptContentTpl = undefined;
 
 
 
 
   /**
   /**
@@ -23,6 +23,11 @@ module.exports = function(crowi, app) {
    * @param {object} res
    * @param {object} res
    */
    */
   const loadAgent = function(req, res) {
   const loadAgent = function(req, res) {
+    // generate swig template
+    if (agentScriptContentTpl == null) {
+      agentScriptContentTpl = swig.compileFile(agentScriptPath);
+    }
+
     const origin = `${req.protocol}://${req.get('host')}`;
     const origin = `${req.protocol}://${req.get('host')}`;
     const styleFilePath = origin + manifest['styles/style-hackmd.css'];
     const styleFilePath = origin + manifest['styles/style-hackmd.css'];
 
 

+ 6 - 2
lib/routes/login-passport.js

@@ -95,12 +95,15 @@ module.exports = function(crowi, app) {
     const ldapAccountId = passportService.getLdapAccountIdFromReq(req);
     const ldapAccountId = passportService.getLdapAccountIdFromReq(req);
     const attrMapUsername = passportService.getLdapAttrNameMappedToUsername();
     const attrMapUsername = passportService.getLdapAttrNameMappedToUsername();
     const attrMapName = passportService.getLdapAttrNameMappedToName();
     const attrMapName = passportService.getLdapAttrNameMappedToName();
+    const attrMapMail = passportService.getLdapAttrNameMappedToMail();
     const usernameToBeRegistered = ldapAccountInfo[attrMapUsername];
     const usernameToBeRegistered = ldapAccountInfo[attrMapUsername];
     const nameToBeRegistered = ldapAccountInfo[attrMapName];
     const nameToBeRegistered = ldapAccountInfo[attrMapName];
+    const mailToBeRegistered = ldapAccountInfo[attrMapMail];
     const userInfo = {
     const userInfo = {
       'id': ldapAccountId,
       'id': ldapAccountId,
       'username': usernameToBeRegistered,
       'username': usernameToBeRegistered,
-      'name': nameToBeRegistered
+      'name': nameToBeRegistered,
+      'email': mailToBeRegistered,
     };
     };
 
 
     const externalAccount = await getOrCreateUser(req, res, next, userInfo, providerId);
     const externalAccount = await getOrCreateUser(req, res, next, userInfo, providerId);
@@ -304,7 +307,8 @@ module.exports = function(crowi, app) {
         providerId,
         providerId,
         userInfo.id,
         userInfo.id,
         userInfo.username,
         userInfo.username,
-        userInfo.name
+        userInfo.name,
+        userInfo.email,
       );
       );
       return externalAccount;
       return externalAccount;
     }
     }

+ 10 - 0
lib/service/passport.js

@@ -154,6 +154,16 @@ class PassportService {
     const config = this.crowi.config;
     const config = this.crowi.config;
     return config.crowi['security:passport-ldap:attrMapName'] || '';
     return config.crowi['security:passport-ldap:attrMapName'] || '';
   }
   }
+  /**
+   * return attribute name for mapping to name of Crowi DB
+   *
+   * @returns
+   * @memberof PassportService
+   */
+  getLdapAttrNameMappedToMail() {
+    const config = this.crowi.config;
+    return config.crowi['security:passport-ldap:attrMapMail'] || 'mail';
+  }
 
 
   /**
   /**
    * CAUTION: this method is capable to use only when `req.body.loginForm` is not null
    * CAUTION: this method is capable to use only when `req.body.loginForm` is not null

+ 17 - 6
lib/views/admin/widget/passport/ldap.html

@@ -120,7 +120,6 @@
       <h4>Attribute Mapping ({{ t("security_setting.optional") }})</h4>
       <h4>Attribute Mapping ({{ t("security_setting.optional") }})</h4>
 
 
       <div class="form-group">
       <div class="form-group">
-        <div class="row">
         <label for="settingForm[security:passport-ldap:attrMapUsername]" class="col-xs-3 control-label">username</label>
         <label for="settingForm[security:passport-ldap:attrMapUsername]" class="col-xs-3 control-label">username</label>
         <div class="col-xs-6">
         <div class="col-xs-6">
           <input class="form-control" type="text" placeholder="Default: uid"
           <input class="form-control" type="text" placeholder="Default: uid"
@@ -131,9 +130,9 @@
             </small>
             </small>
           </p>
           </p>
         </div>
         </div>
-        </div>
+      </div>
 
 
-        <div class="row">
+      <div class="form-group">
         <div class="col-xs-6 col-xs-offset-3">
         <div class="col-xs-6 col-xs-offset-3">
           <div class="checkbox checkbox-info">
           <div class="checkbox checkbox-info">
             <input type="checkbox" id="cbSameUsernameTreatedAsIdenticalUser" name="settingForm[security:passport-ldap:isSameUsernameTreatedAsIdenticalUser]" value="1"
             <input type="checkbox" id="cbSameUsernameTreatedAsIdenticalUser" name="settingForm[security:passport-ldap:isSameUsernameTreatedAsIdenticalUser]" value="1"
@@ -148,11 +147,23 @@
             </p>
             </p>
           </div>
           </div>
         </div>
         </div>
+      </div>
+
+      <div class="form-group">
+        <label for="settingForm[security:passport-ldap:attrMapMail]" class="col-xs-3 control-label">Mail</label>
+        <div class="col-xs-6">
+          <input class="form-control" type="text" placeholder="Default: mail"
+              name="settingForm[security:passport-ldap:attrMapMail]" value="{{ settingForm['security:passport-ldap:attrMapMail'] || '' }}">
+          <p class="help-block">
+            <small>
+              {{ t("security_setting.ldap.mail_detail") }}
+            </small>
+          </p>
         </div>
         </div>
       </div>
       </div>
 
 
-      <div class="row">
-        <label for="settingForm[security:passport-ldap:attrMapName]" class="col-xs-3 control-label">name</label>
+      <div class="form-group">
+        <label for="settingForm[security:passport-ldap:attrMapName]" class="col-xs-3 control-label">Name</label>
         <div class="col-xs-6">
         <div class="col-xs-6">
           <input class="form-control" type="text"
           <input class="form-control" type="text"
               name="settingForm[security:passport-ldap:attrMapName]" value="{{ settingForm['security:passport-ldap:attrMapName'] || '' }}">
               name="settingForm[security:passport-ldap:attrMapName]" value="{{ settingForm['security:passport-ldap:attrMapName'] || '' }}">
@@ -162,7 +173,7 @@
             </small>
             </small>
           </p>
           </p>
         </div>
         </div>
-        </div>
+      </div>
 
 
       <h4>{{ t("security_setting.ldap.group_search_filter") }} ({{ t("security_setting.optional") }})</h4>
       <h4>{{ t("security_setting.ldap.group_search_filter") }} ({{ t("security_setting.optional") }})</h4>
 
 

+ 24 - 14
resource/js/components/PageEditor/CodeMirrorEditor.js

@@ -57,7 +57,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
       isGfmMode: this.props.isGfmMode,
       isGfmMode: this.props.isGfmMode,
       isEnabledEmojiAutoComplete: false,
       isEnabledEmojiAutoComplete: false,
       isLoadingKeymap: false,
       isLoadingKeymap: false,
-      additionalClass: '',
+      additionalClassSet: new Set(),
     };
     };
 
 
     this.init();
     this.init();
@@ -150,10 +150,15 @@ export default class CodeMirrorEditor extends AbstractEditor {
    * @inheritDoc
    * @inheritDoc
    */
    */
   setGfmMode(bool) {
   setGfmMode(bool) {
+    // update state
+    const additionalClassSet = this.state.additionalClassSet;
     this.setState({
     this.setState({
       isGfmMode: bool,
       isGfmMode: bool,
       isEnabledEmojiAutoComplete: bool,
       isEnabledEmojiAutoComplete: bool,
+      additionalClassSet,
     });
     });
+
+    // update CodeMirror option
     const mode = bool ? 'gfm' : undefined;
     const mode = bool ? 'gfm' : undefined;
     this.getCodeMirror().setOption('mode', mode);
     this.getCodeMirror().setOption('mode', mode);
   }
   }
@@ -388,11 +393,21 @@ export default class CodeMirrorEditor extends AbstractEditor {
 
 
   cursorHandler(editor, event) {
   cursorHandler(editor, event) {
     const strFromBol = this.getStrFromBol();
     const strFromBol = this.getStrFromBol();
+
+    const autoformatTableClass = 'autoformat-markdown-table-activated';
+    const additionalClassSet = this.state.additionalClassSet;
+    const hasCustomClass = additionalClassSet.has(autoformatTableClass);
     if (mtu.isEndOfLine(editor) && mtu.linePartOfTableRE.test(strFromBol)) {
     if (mtu.isEndOfLine(editor) && mtu.linePartOfTableRE.test(strFromBol)) {
-      this.setState({additionalClass: 'autoformat-markdown-table-activated'});
+      if (!hasCustomClass) {
+        additionalClassSet.add(autoformatTableClass);
+        this.setState({additionalClassSet});
+      }
     }
     }
     else {
     else {
-      this.setState({additionalClass: ''});
+      if (hasCustomClass) {
+        additionalClassSet.delete(autoformatTableClass);
+        this.setState({additionalClassSet});
+      }
     }
     }
   }
   }
 
 
@@ -415,23 +430,17 @@ export default class CodeMirrorEditor extends AbstractEditor {
     }
     }
   }
   }
 
 
-  getOverlayStyle() {
-    return {
-      position: 'absolute',
-      zIndex: 4,  // forward than .CodeMirror-gutters
+  renderLoadingKeymapOverlay() {
+    const style = {
       top: 0,
       top: 0,
       right: 0,
       right: 0,
       bottom: 0,
       bottom: 0,
       left: 0,
       left: 0,
     };
     };
-  }
-
-  renderLoadingKeymapOverlay() {
-    const overlayStyle = this.getOverlayStyle();
 
 
     return this.state.isLoadingKeymap
     return this.state.isLoadingKeymap
-      ? <div style={overlayStyle} className="loading-keymap overlay">
-          <span className="overlay-content">
+      ? <div className="overlay overlay-loading-keymap">
+          <span style={style} className="overlay-content">
             <div className="speeding-wheel d-inline-block"></div> Loading Keymap ...
             <div className="speeding-wheel d-inline-block"></div> Loading Keymap ...
           </span>
           </span>
         </div>
         </div>
@@ -444,12 +453,13 @@ export default class CodeMirrorEditor extends AbstractEditor {
       theme: 'elegant',
       theme: 'elegant',
       lineNumbers: true,
       lineNumbers: true,
     };
     };
+    const additionalClasses = Array.from(this.state.additionalClassSet).join(' ');
     const editorOptions = Object.assign(defaultEditorOptions, this.props.editorOptions || {});
     const editorOptions = Object.assign(defaultEditorOptions, this.props.editorOptions || {});
 
 
     return <React.Fragment>
     return <React.Fragment>
       <ReactCodeMirror
       <ReactCodeMirror
         ref="cm"
         ref="cm"
-        className={this.state.additionalClass}
+        className={additionalClasses}
         editorDidMount={(editor) => {
         editorDidMount={(editor) => {
           // add event handlers
           // add event handlers
           editor.on('paste', this.pasteHandler);
           editor.on('paste', this.pasteHandler);

+ 1 - 1
resource/js/components/PageEditor/Editor.js

@@ -187,7 +187,7 @@ export default class Editor extends AbstractEditor {
 
 
   renderDropzoneOverlay() {
   renderDropzoneOverlay() {
     return (
     return (
-      <div className="overlay">
+      <div className="overlay overlay-dropzone-active">
         {this.state.isUploading &&
         {this.state.isUploading &&
           <span className="overlay-content">
           <span className="overlay-content">
             <div className="speeding-wheel d-inline-block"></div>
             <div className="speeding-wheel d-inline-block"></div>

+ 46 - 80
resource/styles/scss/_editor-attachment.scss

@@ -1,45 +1,6 @@
-.editor-container {
-  .overlay {
-    // layout
-    display: flex;
-    justify-content: center;
-    align-items: center;
-
-    position: absolute;
-    z-index: 7;  // forward than .CodeMirror-vscrollbar
-    top: 0;
-    right: 0;
-    bottom: 0;
-    left: 0;
-  }
-
-  .overlay-content {
-    padding: 0.5em;
-  }
+@import 'editor-overlay';
 
 
-  .page-editor-editor-container {
-    .overlay-content {
-      font-size: 2.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;
-    }
-  }
-  // add icon on cursor
-  .autoformat-markdown-table-activated .CodeMirror-cursor {
-    &:after {
-      font-family: 'FontAwesome';
-      content: '\f0ce';
-    }
-  }
+.editor-container {
 
 
   // for Dropzone
   // for Dropzone
   .dropzone {
   .dropzone {
@@ -53,27 +14,32 @@
 
 
     position: relative;   // against .overlay position: absolute
     position: relative;   // against .overlay position: absolute
 
 
+    @include overlay-processing-style(overlay-dropzone-active, 2.5em);
+
     // unuploadable or rejected
     // unuploadable or rejected
     &.dropzone-unuploadable, &.dropzone-rejected {
     &.dropzone-unuploadable, &.dropzone-rejected {
-      .overlay {
+      .overlay.overlay-dropzone-active {
         background: rgba(200,200,200,0.8);
         background: rgba(200,200,200,0.8);
-      }
-      .overlay-content {
-        color: #444;
+
+        .overlay-content {
+          color: #444;
+        }
       }
       }
     }
     }
     // uploading
     // uploading
     &.dropzone-uploading {
     &.dropzone-uploading {
-      @include overlay-processing-style();
+      @include overlay-processing-style(overlay-dropzone-active, 2.5em);
     }
     }
 
 
     // unuploadable
     // unuploadable
     &.dropzone-unuploadable {
     &.dropzone-unuploadable {
-      .overlay-content {
-        // insert content
-        @include insertSimpleLineIcons("\e617");  // icon-exclamation
-        &:after {
-          content: "File uploading is disabled";
+      .overlay.overlay-dropzone-active {
+        .overlay-content {
+          // insert content
+          @include insertSimpleLineIcons("\e617");  // icon-exclamation
+          &:after {
+            content: "File uploading is disabled";
+          }
         }
         }
       }
       }
     }
     }
@@ -81,48 +47,48 @@
     &.dropzone-uploadable {
     &.dropzone-uploadable {
       // accepted
       // accepted
       &.dropzone-accepted:not(.dropzone-rejected) {
       &.dropzone-accepted:not(.dropzone-rejected) {
-        .overlay {
+        .overlay.overlay-dropzone-active {
           border: 4px dashed #ccc;
           border: 4px dashed #ccc;
-        }
-        .overlay-content {
-          // insert content
-          @include insertSimpleLineIcons("\e084");  // icon-cloud-upload
-          &:after {
-            content: "Drop here to upload";
+
+          .overlay-content {
+            // insert content
+            @include insertSimpleLineIcons("\e084");  // icon-cloud-upload
+            &:after {
+              content: "Drop here to upload";
+            }
+            // style
+            color: #666;
+            background: rgba(200,200,200,0.8);
           }
           }
-          // style
-          color: #666;
-          background: rgba(200,200,200,0.8);
         }
         }
       }
       }
       // file type mismatch
       // file type mismatch
-      &.dropzone-rejected:not(.dropzone-uploadablefile) .overlay-content {
-        // insert content
-        @include insertSimpleLineIcons("\e032");  // icon-picture
-        &:after {
-          content: "Only an image file is allowed";
+      &.dropzone-rejected:not(.dropzone-uploadablefile) {
+        .overlay.overlay-dropzone-active {
+          .overlay-content {
+            // insert content
+            @include insertSimpleLineIcons("\e032");  // icon-picture
+            &:after {
+              content: "Only an image file is allowed";
+            }
+          }
         }
         }
       }
       }
       // multiple files
       // multiple files
-      &.dropzone-accepted.dropzone-rejected .overlay-content {
-        // insert content
-        @include insertSimpleLineIcons("\e617");  // icon-exclamation
-        &:after {
-          content: "Only 1 file is allowed";
+      &.dropzone-accepted.dropzone-rejected {
+        .overlay.overlay-dropzone-active {
+          .overlay-content {
+            // insert content
+            @include insertSimpleLineIcons("\e617");  // icon-exclamation
+            &:after {
+              content: "Only 1 file is allowed";
+            }
+          }
         }
         }
       }
       }
     }
     }
   } // end of.dropzone
   } // end of.dropzone
 
 
-  .textarea-editor {
-    border: none;
-    font-family: monospace;
-  }
-
-  .loading-keymap {
-    @include overlay-processing-style();
-  }
-
   .btn-open-dropzone {
   .btn-open-dropzone {
     z-index: 2;
     z-index: 2;
     font-size: small;
     font-size: small;

+ 37 - 0
resource/styles/scss/_editor-overlay.scss

@@ -0,0 +1,37 @@
+@mixin overlay-processing-style($additionalSelector, $contentFontSize: inherit, $contentPadding: inherit) {
+  .overlay.#{$additionalSelector} {
+    background: rgba(255,255,255,0.5);
+
+    .overlay-content {
+      background: rgba(200,200,200,0.5);
+      color: #444;
+      font-size: $contentFontSize;
+      padding: $contentPadding;
+    }
+  }
+}
+
+// overlay in .editor-container
+.editor-container {
+  .overlay {
+    // layout
+    display: flex;
+    justify-content: center;
+    align-items: center;
+
+    position: absolute;
+    z-index: 7;  // forward than .CodeMirror-vscrollbar
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+
+    .overlay-content {
+      padding: 0.5em;
+    }
+  }
+
+  // loading keymap
+  @include overlay-processing-style(overlay-loading-keymap, 2.5em, 0.3em);
+
+}

+ 7 - 98
resource/styles/scss/_on-edit.scss

@@ -1,3 +1,5 @@
+@import 'editor-overlay';
+
 body:not(.on-edit) {
 body:not(.on-edit) {
   // hide #page-form
   // hide #page-form
   #page-form {
   #page-form {
@@ -179,29 +181,6 @@ 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;
-        }
-      }
       // add icon on cursor
       // add icon on cursor
       .autoformat-markdown-table-activated .CodeMirror-cursor {
       .autoformat-markdown-table-activated .CodeMirror-cursor {
         &:after {
         &:after {
@@ -210,86 +189,11 @@ body.on-edit {
         }
         }
       }
       }
 
 
-      // for Dropzone
-      .dropzone {
-        @mixin insertSimpleLineIcons($code) {
-          &:before {
-            margin-right: 0.2em;
-            font-family: 'simple-line-icons';
-            content: $code;
-          }
-        }
-
-        // unuploadable or rejected
-        &.dropzone-unuploadable, &.dropzone-rejected {
-          .overlay {
-            background: rgba(200,200,200,0.8);
-          }
-          .overlay-content {
-            color: #444;
-          }
-        }
-        // uploading
-        &.dropzone-uploading {
-          @include overlay-processing-style();
-        }
-
-        // unuploadable
-        &.dropzone-unuploadable {
-          .overlay-content {
-            // insert content
-            @include insertSimpleLineIcons("\e617");  // icon-exclamation
-            &:after {
-              content: "File uploading is disabled";
-            }
-          }
-        }
-        // uploadable
-        &.dropzone-uploadable {
-          // accepted
-          &.dropzone-accepted:not(.dropzone-rejected) {
-            .overlay {
-              border: 4px dashed #ccc;
-            }
-            .overlay-content {
-              // insert content
-              @include insertSimpleLineIcons("\e084");  // icon-cloud-upload
-              &:after {
-                content: "Drop here to upload";
-              }
-              // style
-              color: #666;
-              background: rgba(200,200,200,0.8);
-            }
-          }
-          // file type mismatch
-          &.dropzone-rejected:not(.dropzone-uploadablefile) .overlay-content {
-            // insert content
-            @include insertSimpleLineIcons("\e032");  // icon-picture
-            &:after {
-              content: "Only an image file is allowed";
-            }
-          }
-          // multiple files
-          &.dropzone-accepted.dropzone-rejected .overlay-content {
-            // insert content
-            @include insertSimpleLineIcons("\e617");  // icon-exclamation
-            &:after {
-              content: "Only 1 file is allowed";
-            }
-          }
-        }
-      } // end of.dropzone
-
       .textarea-editor {
       .textarea-editor {
         border: none;
         border: none;
         font-family: monospace;
         font-family: monospace;
       }
       }
 
 
-      .loading-keymap {
-        @include overlay-processing-style();
-      }
-
     }
     }
     .page-editor-preview-container {
     .page-editor-preview-container {
     }
     }
@@ -396,3 +300,8 @@ body.on-edit {
   }
   }
 
 
 }
 }
+
+// overwrite .CodeMirror-placeholder
+.CodeMirror pre.CodeMirror-placeholder {
+  color: $text-muted;
+}