فهرست منبع

Merge pull request #24 from weseek/feat/custom-css

Feat/custom css
Yuki Takei 9 سال پیش
والد
کامیت
affbee4006

+ 9 - 0
lib/form/admin/customcss.js

@@ -0,0 +1,9 @@
+'use strict';
+
+var form = require('express-form')
+  , field = form.field
+  ;
+
+module.exports = form(
+  field('settingForm[customize:css]')
+);

+ 1 - 0
lib/form/index.js

@@ -17,6 +17,7 @@ module.exports = {
     google: require('./admin/google'),
     google: require('./admin/google'),
     plugin: require('./admin/plugin'),
     plugin: require('./admin/plugin'),
     markdown: require('./admin/markdown'),
     markdown: require('./admin/markdown'),
+    customcss: require('./admin/customcss'),
     userInvite: require('./admin/userInvite'),
     userInvite: require('./admin/userInvite'),
     slackSetting: require('./admin/slackSetting'),
     slackSetting: require('./admin/slackSetting'),
   },
   },

+ 23 - 0
lib/models/config.js

@@ -1,6 +1,7 @@
 module.exports = function(crowi) {
 module.exports = function(crowi) {
   var mongoose = require('mongoose')
   var mongoose = require('mongoose')
     , debug = require('debug')('crowi:models:config')
     , debug = require('debug')('crowi:models:config')
+    , uglifycss = require('uglifycss')
     , ObjectId = mongoose.Schema.Types.ObjectId
     , ObjectId = mongoose.Schema.Types.ObjectId
     , configSchema
     , configSchema
     , Config
     , Config
@@ -44,6 +45,7 @@ module.exports = function(crowi) {
 
 
       'plugin:isEnabledPlugins' : true,
       'plugin:isEnabledPlugins' : true,
 
 
+      'customize:css' : '',
     };
     };
   }
   }
 
 
@@ -76,6 +78,9 @@ module.exports = function(crowi) {
 
 
     originalConfig[ns] = newNSConfig;
     originalConfig[ns] = newNSConfig;
     crowi.setConfig(originalConfig);
     crowi.setConfig(originalConfig);
+
+    // uglify and store
+    Config.generateUglifiedCustomCss(originalConfig);
   };
   };
 
 
   // Execute only once for installing application
   // Execute only once for installing application
@@ -175,6 +180,10 @@ module.exports = function(crowi) {
         });
         });
 
 
         debug('Config loaded', config);
         debug('Config loaded', config);
+
+        // uglify and store
+        Config.generateUglifiedCustomCss(config);
+
         return callback(null, config);
         return callback(null, config);
       });
       });
   };
   };
@@ -230,6 +239,20 @@ module.exports = function(crowi) {
     return config.markdown['markdown:isEnabledLinebreaksInComments'];
     return config.markdown['markdown:isEnabledLinebreaksInComments'];
   };
   };
 
 
+  /**
+   * uglify store custom css strings
+   */
+  configSchema.statics.generateUglifiedCustomCss = function(config)
+  {
+    var rawCss = config.crowi['customize:css'] || getArrayForInstalling()['customize:css'];
+    this.uglifiedCustomCss = uglifycss.processString(rawCss);
+  }
+
+  configSchema.statics.customCss = function(config)
+  {
+    return this.uglifiedCustomCss;
+  }
+
   configSchema.statics.hasSlackConfig = function(config)
   configSchema.statics.hasSlackConfig = function(config)
   {
   {
     if (!config.notification) {
     if (!config.notification) {

+ 22 - 0
lib/routes/admin.js

@@ -114,6 +114,17 @@ module.exports = function(crowi, app) {
     }
     }
   };
   };
 
 
+  // app.get('/admin/customize' , admin.customize.index);
+  actions.customize = {};
+  actions.customize.index = function(req, res) {
+    var settingForm;
+    settingForm = Config.setupCofigFormData('crowi', req.config);
+
+    return res.render('admin/customize', {
+      settingForm: settingForm,
+    });
+  };
+
   // app.get('/admin/notification'               , admin.notification.index);
   // app.get('/admin/notification'               , admin.notification.index);
   actions.notification = {};
   actions.notification = {};
   actions.notification.index = function(req, res) {
   actions.notification.index = function(req, res) {
@@ -408,6 +419,17 @@ module.exports = function(crowi, app) {
     }
     }
   };
   };
 
 
+  actions.api.customizeSetting = function(req, res) {
+    var form = req.form.settingForm;
+
+    if (req.form.isValid) {
+      debug('form content', form);
+      return saveSetting(req, res, form);
+    } else {
+      return res.json({status: false, message: req.form.errors.join('\n')});
+    }
+  }
+
   // app.post('/_api/admin/notifications.add'    , admin.api.notificationAdd);
   // app.post('/_api/admin/notifications.add'    , admin.api.notificationAdd);
   actions.api.notificationAdd = function(req, res) {
   actions.api.notificationAdd = function(req, res) {
     var UpdatePost = crowi.model('UpdatePost');
     var UpdatePost = crowi.model('UpdatePost');

+ 4 - 0
lib/routes/index.js

@@ -51,6 +51,10 @@ module.exports = function(crowi, app) {
   app.get('/admin/markdown'                   , loginRequired(crowi, app) , middleware.adminRequired() , admin.markdown.index);
   app.get('/admin/markdown'                   , loginRequired(crowi, app) , middleware.adminRequired() , admin.markdown.index);
   app.post('/admin/markdown/lineBreaksSetting', loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.markdown, admin.markdown.lineBreaksSetting);
   app.post('/admin/markdown/lineBreaksSetting', loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.markdown, admin.markdown.lineBreaksSetting);
 
 
+  // markdown admin
+  app.get('/admin/customize'            , loginRequired(crowi, app) , middleware.adminRequired() , admin.customize.index);
+  app.post('/_api/admin/customize/css'  , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.customcss, admin.api.customizeSetting);
+
   // search admin
   // search admin
   app.get('/admin/search'              , loginRequired(crowi, app) , middleware.adminRequired() , admin.search.index);
   app.get('/admin/search'              , loginRequired(crowi, app) , middleware.adminRequired() , admin.search.index);
   app.post('/admin/search/build'       , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.search.buildIndex);
   app.post('/admin/search/build'       , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.search.buildIndex);

+ 4 - 0
lib/util/swigFunctions.js

@@ -37,6 +37,10 @@ module.exports = function(crowi, app, req, locals) {
     return Config.isEnabledLinebreaksInComments(config);
     return Config.isEnabledLinebreaksInComments(config);
   }
   }
 
 
+  locals.customCss = function() {
+    return Config.customCss();
+  }
+
   locals.slackConfigured = function() {
   locals.slackConfigured = function() {
     var config = crowi.getConfig()
     var config = crowi.getConfig()
     if (Config.hasSlackToken(config)) {
     if (Config.hasSlackToken(config)) {

+ 161 - 0
lib/views/admin/customize.html

@@ -0,0 +1,161 @@
+{% extends '../layout/admin.html' %}
+
+{% block html_title %}カスタマイズ · {% endblock %}
+
+{% block html_additional_headers %}
+  <!-- CodeMirror -->
+  <link rel="stylesheet" href="https://cdn.jsdelivr.net/g/codemirror@4.5.0(codemirror.css+addon/hint/show-hint.css)">
+  <link rel="stylesheet" href="https://cdn.jsdelivr.net/jquery.ui/1.11.4/jquery-ui.min.css">
+  <link rel="stylesheet" href="https://cdn.jsdelivr.net/codemirror/4.5.0/theme/eclipse.css">
+  <style>
+    .CodeMirror {
+      border: 1px solid #eee;
+    }
+  </style>
+{% endblock %}
+
+
+{% block content_head %}
+<div class="header-wrap">
+  <header id="page-header">
+    <h1 class="title" id="">カスタマイズ</h1>
+  </header>
+</div>
+{% endblock %}
+
+{% block content_main %}
+<div class="content-main">
+  {% set smessage = req.flash('successMessage') %}
+  {% if smessage.length %}
+  <div class="alert alert-success">
+    {{ smessage }}
+  </div>
+  {% endif %}
+
+  {% set emessage = req.flash('errorMessage') %}
+  {% if emessage.length %}
+  <div class="alert alert-danger">
+    {{ emessage }}
+  </div>
+  {% endif %}
+
+  <div class="row">
+    <div class="col-md-3">
+      {% include './widget/menu.html' with {current: 'customize'} %}
+    </div>
+    <div class="col-md-9">
+
+      <form action="/_api/admin/customize/css" method="post" class="form-horizontal" id="cutomcssSettingForm" role="form">
+      <fieldset>
+        <legend>カスタムCSS</legend>
+
+        <p class="help-block">
+          システム全体に適用されるCSSを記述できます。<br>
+          変更の反映はページの更新が必要です。
+        </p>
+
+        <div class="form-group">
+          <div class="col-xs-12">
+            <textarea id="taCustomCss" class="form-control" type="textarea" name="settingForm[customize:css]" rows="20">{{ settingForm['customize:css'] }}</textarea>
+          </div>
+          <div class="col-xs-12">
+            <p class="help-block text-right">
+              <i class="fa fa-fw fa-keyboard-o" aria-hidden="true"></i>
+              Ctrl+Space でコード補完
+            </p>
+          </div>
+        </div>
+
+        <div class="form-group">
+          <div class="col-xs-offset-5 col-xs-6">
+            <input type="hidden" name="_csrf" value="{{ csrf() }}">
+            <button type="submit" class="btn btn-primary">更新</button>
+          </div>
+        </div>
+
+      </fieldset>
+      </form>
+
+    </div>
+  </div>
+
+  <script>
+    $('#cutomcssSettingForm').each(function() {
+      $(this).submit(function()
+      {
+        function showMessage(formId, msg, status) {
+          $('#' + formId + ' .alert').remove();
+
+          if (!status) {
+            status = 'success';
+          }
+          var $message = $('<p class="alert"></p>');
+          $message.addClass('alert-' + status);
+          $message.html(msg.replace('\n', '<br>'));
+          $message.insertAfter('#' + formId + ' legend');
+
+          if (status == 'success') {
+            setTimeout(function()
+            {
+              $message.fadeOut({
+                complete: function() {
+                  $message.remove();
+                }
+              });
+            }, 5000);
+          }
+        }
+
+        var $form = $(this);
+        var $id = $form.attr('id');
+        var $button = $('button', this);
+        $button.attr('disabled', 'disabled');
+        var jqxhr = $.post($form.attr('action'), $form.serialize(), function(data)
+          {
+            if (data.status) {
+              showMessage($id, '更新しました');
+            } else {
+              showMessage($id, data.message, 'danger');
+            }
+          })
+          .fail(function() {
+            showMessage($id, 'エラーが発生しました', 'danger');
+          })
+          .always(function() {
+            $button.prop('disabled', false);
+        });
+        return false;
+      });
+    });
+
+  </script>
+
+  <!-- CodeMirror -->
+  <script src="https://cdn.jsdelivr.net/g/codemirror@4.5.0(codemirror.min.js+addon/lint/css-lint.js+mode/css/css.js+addon/hint/css-hint.js+addon/hint/show-hint.js+addon/edit/matchbrackets.js+addon/edit/closebrackets.js),jquery.ui@1.11.4"></script>
+  <script>
+    var editor = CodeMirror.fromTextArea(document.getElementById('taCustomCss'), {
+      mode: "css",
+      lineNumbers: true,
+      tabSize: 2,
+      indentUnit: 2,
+      theme: 'eclipse',
+      matchBrackets: true,
+      autoCloseBrackets: true,
+      extraKeys: {"Ctrl-Space": "autocomplete"},
+    });
+    editor.on('change', function(cm, change) {
+      cm.save();
+    });
+    // resizable with jquery.ui
+    $(editor.getWrapperElement()).resizable({
+      resize: function() {
+        editor.setSize($(this).width(), $(this).height());
+      }
+    });
+  </script>
+
+</div>
+{% endblock content_main %}
+
+{% block content_footer %}
+{% endblock content_footer %}

+ 2 - 1
lib/views/admin/widget/menu.html

@@ -4,7 +4,8 @@
 <ul class="nav nav-pills nav-stacked">
 <ul class="nav nav-pills nav-stacked">
   <li class="{% if current == 'index'%}active{% endif %}"><a href="/admin"><i class="fa fa-cube"></i> Wiki管理トップ</a></li>
   <li class="{% if current == 'index'%}active{% endif %}"><a href="/admin"><i class="fa fa-cube"></i> Wiki管理トップ</a></li>
   <li class="{% if current == 'app'%}active{% endif %}"><a href="/admin/app"><i class="fa fa-gears"></i> アプリ設定</a></li>
   <li class="{% if current == 'app'%}active{% endif %}"><a href="/admin/app"><i class="fa fa-gears"></i> アプリ設定</a></li>
-  <li class="{% if current == 'markdown'%}active{% endif %}"><a href="/admin/markdown"><i class="fa fa-gears"></i> Markdown設定</a></li>
+  <li class="{% if current == 'markdown'%}active{% endif %}"><a href="/admin/markdown"><i class="fa fa-pencil"></i> Markdown設定</a></li>
+  <li class="{% if current == 'customize'%}active{% endif %}"><a href="/admin/customize"><i class="fa fa-object-group"></i> カスタマイズ</a></li>
   <li class="{% if current == 'notification'%}active{% endif %}"><a href="/admin/notification"><i class="fa fa-bell"></i> 通知設定</a></li>
   <li class="{% if current == 'notification'%}active{% endif %}"><a href="/admin/notification"><i class="fa fa-bell"></i> 通知設定</a></li>
   <li class="{% if current == 'user'%}active{% endif %}"><a href="/admin/users"><i class="fa fa-users"></i> ユーザー管理</a></li>
   <li class="{% if current == 'user'%}active{% endif %}"><a href="/admin/users"><i class="fa fa-users"></i> ユーザー管理</a></li>
   {% if searchConfigured() %}
   {% if searchConfigured() %}

+ 4 - 0
lib/views/layout/layout.html

@@ -44,6 +44,10 @@
   <link href='//cdn.jsdelivr.net/fontawesome/4.7.0/css/font-awesome.min.css' rel='stylesheet' type='text/css'>
   <link href='//cdn.jsdelivr.net/fontawesome/4.7.0/css/font-awesome.min.css' rel='stylesheet' type='text/css'>
 
 
   {% block html_additional_headers %}{% endblock %}
   {% block html_additional_headers %}{% endblock %}
+
+  <style>
+    {{ customCss() }}
+  </style>
 </head>
 </head>
 {% endblock %}
 {% endblock %}
 
 

+ 1 - 0
package.json

@@ -109,6 +109,7 @@
     "sprintf": "~0.1.5",
     "sprintf": "~0.1.5",
     "style-loader": "^0.16.1",
     "style-loader": "^0.16.1",
     "swig": "~1.4.0",
     "swig": "~1.4.0",
+    "uglifycss": "^0.0.25",
     "webpack": "~2.3.1",
     "webpack": "~2.3.1",
     "webpack-dll-bundles-plugin": "^1.0.0-beta.5",
     "webpack-dll-bundles-plugin": "^1.0.0-beta.5",
     "webpack-merge": "~3.0.0"
     "webpack-merge": "~3.0.0"

+ 4 - 0
yarn.lock

@@ -5575,6 +5575,10 @@ uglify-to-browserify@~1.0.0:
   version "1.0.2"
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
   resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
 
 
+uglifycss@^0.0.25:
+  version "0.0.25"
+  resolved "https://registry.yarnpkg.com/uglifycss/-/uglifycss-0.0.25.tgz#bea72bf4979eacef13a302cf47b2d1af3f344197"
+
 uid-number@^0.0.6:
 uid-number@^0.0.6:
   version "0.0.6"
   version "0.0.6"
   resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
   resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"