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

Merge branch 'master' into feature-search

Sotaro KARASAWA 10 лет назад
Родитель
Сommit
75851565a9

+ 1 - 0
gulpfile.js

@@ -47,6 +47,7 @@ var js = {
     'node_modules/inline-attachment/src/inline-attachment.js',
     'node_modules/inline-attachment/src/inline-attachment.js',
     'node_modules/socket.io-client/socket.io.js',
     'node_modules/socket.io-client/socket.io.js',
     'node_modules/jquery.cookie/jquery.cookie.js',
     'node_modules/jquery.cookie/jquery.cookie.js',
+    'node_modules/diff/dist/diff.js',
     'resource/thirdparty-js/jquery.selection.js',
     'resource/thirdparty-js/jquery.selection.js',
     dirs.jsDist + '/crowi-bundled.js',
     dirs.jsDist + '/crowi-bundled.js',
   ],
   ],

+ 2 - 1
lib/crowi/index.js

@@ -107,7 +107,8 @@ Crowi.prototype.event = function(name, event) {
 
 
 Crowi.prototype.setupDatabase = function() {
 Crowi.prototype.setupDatabase = function() {
   // mongoUri = mongodb://user:password@host/dbname
   // mongoUri = mongodb://user:password@host/dbname
-  var mongoUri = this.env.MONGOLAB_URI ||
+  var mongoUri = this.env.MONGOLAB_URI || // for B.C.
+    this.env.MONGODB_URI || // MONGOLAB changes their env name
     this.env.MONGOHQ_URL ||
     this.env.MONGOHQ_URL ||
     this.env.MONGO_URI ||
     this.env.MONGO_URI ||
     'mongodb://localhost/crowi'
     'mongodb://localhost/crowi'

+ 29 - 19
lib/models/bookmark.js

@@ -12,32 +12,32 @@ module.exports = function(crowi) {
   });
   });
   bookmarkSchema.index({page: 1, user: 1}, {unique: true});
   bookmarkSchema.index({page: 1, user: 1}, {unique: true});
 
 
-  bookmarkSchema.statics.populatePage = function(bookmarks) {
+  bookmarkSchema.statics.populatePage = function(bookmarks, requestUser) {
     var Bookmark = this;
     var Bookmark = this;
     var User = crowi.model('User');
     var User = crowi.model('User');
     var Page = crowi.model('Page');
     var Page = crowi.model('Page');
 
 
-    return new Promise(function(resolve, reject) {
-      Bookmark.populate(bookmarks, {path: 'page'}, function(err, bookmarks) {
-        if (err) {
-          return reject(err);
-        }
-
-        Bookmark.populate(bookmarks, {path: 'page.revision', model: 'Revision'}, function(err, bookmarks) {
-          if (err) {
-            return reject(err);
+    requestUser = requestUser || null;
+
+    // mongoose promise に置き換えてみたものの、こいつは not native promise but original promise だったので
+    // これ以上は置き換えないことにする ...
+    // @see http://eddywashere.com/blog/switching-out-callbacks-with-promises-in-mongoose/
+    return Bookmark.populate(bookmarks, {path: 'page'})
+      .then(function(bookmarks) {
+        return Bookmark.populate(bookmarks, {path: 'page.revision', model: 'Revision'});
+      }).then(function(bookmarks) {
+        // hmm...
+        bookmarks = bookmarks.filter(function(bookmark) {
+          // requestUser を指定しない場合 public のみを返す
+          if (requestUser === null) {
+            return bookmark.page.isPublic();
           }
           }
 
 
-          Bookmark.populate(bookmarks, {path: 'page.revision.author', model: 'User', select: User.USER_PUBLIC_FIELDS}, function(err, bookmarks) {
-            if (err) {
-              return reject(err);
-            }
-
-            return resolve(bookmarks);
-          });
+          return bookmark.page.isGrantedFor(requestUser);
         });
         });
+
+        return Bookmark.populate(bookmarks, {path: 'page.revision.author', model: 'User', select: User.USER_PUBLIC_FIELDS});
       });
       });
-    });
   };
   };
 
 
   // bookmark チェック用
   // bookmark チェック用
@@ -55,9 +55,19 @@ module.exports = function(crowi) {
     });
     });
   };
   };
 
 
+  /**
+   * option = {
+   *  limit: Int
+   *  offset: Int
+   *  requestUser: User
+   * }
+   */
   bookmarkSchema.statics.findByUser = function(user, option) {
   bookmarkSchema.statics.findByUser = function(user, option) {
     var User = crowi.model('User');
     var User = crowi.model('User');
     var Bookmark = this;
     var Bookmark = this;
+    var requestUser = option.requestUser || null;
+
+    debug('Finding bookmark with requesting user:', requestUser);
 
 
     var limit = option.limit || 50;
     var limit = option.limit || 50;
     var offset = option.offset || 0;
     var offset = option.offset || 0;
@@ -78,7 +88,7 @@ module.exports = function(crowi) {
             return resolve(bookmarks);
             return resolve(bookmarks);
           }
           }
 
 
-          return Bookmark.populatePage(bookmarks).then(resolve).catch(reject);
+          return Bookmark.populatePage(bookmarks, requestUser).then(resolve);
         });
         });
     });
     });
   };
   };

+ 27 - 15
lib/models/page.js

@@ -427,6 +427,9 @@ module.exports = function(crowi) {
     });
     });
   };
   };
 
 
+  /**
+   * とりあえず、公開ページであり、redirectTo が無いものだけを出すためだけのAPI
+   */
   pageSchema.statics.findListByCreator = function(user, option) {
   pageSchema.statics.findListByCreator = function(user, option) {
     var Page = this;
     var Page = this;
     var User = crowi.model('User');
     var User = crowi.model('User');
@@ -435,7 +438,7 @@ module.exports = function(crowi) {
 
 
     return new Promise(function(resolve, reject) {
     return new Promise(function(resolve, reject) {
       Page
       Page
-        .find({ creator: user._id, grant: GRANT_PUBLIC })
+        .find({ creator: user._id, grant: GRANT_PUBLIC, redirectTo: null })
         .sort({createdAt: -1})
         .sort({createdAt: -1})
         .skip(offset)
         .skip(offset)
         .limit(limit)
         .limit(limit)
@@ -479,9 +482,17 @@ module.exports = function(crowi) {
       .stream();
       .stream();
   };
   };
 
 
+  /**
+   * findListByStartWith
+   *
+   * If `path` has `/` at the end, returns '{path}/*' and '{path}' self.
+   * If `path` doesn't have `/` at the end, returns '{path}*'
+   * e.g.
+   */
   pageSchema.statics.findListByStartWith = function(path, userData, option) {
   pageSchema.statics.findListByStartWith = function(path, userData, option) {
     var Page = this;
     var Page = this;
     var User = crowi.model('User');
     var User = crowi.model('User');
+    var pathCondition = [];
 
 
     if (!option) {
     if (!option) {
       option = {sort: 'updatedAt', desc: -1, offset: 0, limit: 50};
       option = {sort: 'updatedAt', desc: -1, offset: 0, limit: 50};
@@ -497,10 +508,15 @@ module.exports = function(crowi) {
     var queryReg = new RegExp('^' + path);
     var queryReg = new RegExp('^' + path);
     var sliceOption = option.revisionSlice || {$slice: 1};
     var sliceOption = option.revisionSlice || {$slice: 1};
 
 
+    pathCondition.push({path: queryReg});
+    if (path.match(/\/$/)) {
+      debug('Page list by ending with /, so find also upper level page');
+      pathCondition.push({path: path.substr(0, path.length -1)});
+    }
+
     return new Promise(function(resolve, reject) {
     return new Promise(function(resolve, reject) {
       // FIXME: might be heavy
       // FIXME: might be heavy
       var q = Page.find({
       var q = Page.find({
-          path: queryReg,
           redirectTo: null,
           redirectTo: null,
           $or: [
           $or: [
             {grant: null},
             {grant: null},
@@ -511,23 +527,19 @@ module.exports = function(crowi) {
           ],
           ],
         })
         })
         .populate('revision')
         .populate('revision')
+        .and({
+          $or: pathCondition
+        })
         .sort(sortOpt)
         .sort(sortOpt)
         .skip(opt.offset)
         .skip(opt.offset)
         .limit(opt.limit);
         .limit(opt.limit);
 
 
-      q.exec(function(err, pages) {
-        if (err) {
-          return reject(err);
-        }
-
-        Page.populate(pages, {path: 'revision.author', model: 'User', select: User.USER_PUBLIC_FIELDS}, function(err, data) {
-          if (err) {
-            return reject(err);
-          }
-
-          return resolve(data);
-        });
-      });
+      q.exec()
+      .then(function(pages) {
+        Page.populate(pages, {path: 'revision.author', model: 'User', select: User.USER_PUBLIC_FIELDS})
+        .then(resolve)
+        .catch(reject);
+      })
     });
     });
   };
   };
 
 

+ 38 - 8
lib/models/revision.js

@@ -21,6 +21,44 @@ module.exports = function(crowi) {
       });
       });
   };
   };
 
 
+  revisionSchema.statics.findRevision = function(id) {
+    var Revision = this;
+
+    return new Promise(function(resolve, reject) {
+      Revision.findById(id)
+        .populate('author')
+        .exec(function(err, data) {
+          if (err) {
+            return reject(err);
+          }
+
+          return resolve(data);
+        });
+      });
+  };
+
+  revisionSchema.statics.findRevisions = function(ids) {
+    var Revision = this;
+
+    if (!Array.isArray(ids)) {
+      return Promise.reject('The argument was not Array.');
+    }
+
+    return new Promise(function(resolve, reject) {
+      Revision
+        .find({ _id: { $in: ids }})
+        .sort({createdAt: -1})
+        .populate('author')
+        .exec(function(err, revisions) {
+          if (err) {
+            return reject(err);
+          }
+
+          return resolve(revisions);
+        });
+    });
+  };
+
   revisionSchema.statics.findRevisionList = function(path, options) {
   revisionSchema.statics.findRevisionList = function(path, options) {
     var Revision = this;
     var Revision = this;
 
 
@@ -52,14 +90,6 @@ module.exports = function(crowi) {
     });
     });
   };
   };
 
 
-  revisionSchema.statics.findRevision = function(id, cb) {
-    this.findById(id)
-      .populate('author')
-      .exec(function(err, data) {
-        cb(err, data);
-      });
-  };
-
   revisionSchema.statics.prepareRevision = function(pageData, body, user, options) {
   revisionSchema.statics.prepareRevision = function(pageData, body, user, options) {
     var Revision = this;
     var Revision = this;
 
 

+ 4 - 0
lib/routes/index.js

@@ -11,6 +11,7 @@ module.exports = function(crowi, app) {
     , attachment= require('./attachment')(crowi, app)
     , attachment= require('./attachment')(crowi, app)
     , comment   = require('./comment')(crowi, app)
     , comment   = require('./comment')(crowi, app)
     , bookmark  = require('./bookmark')(crowi, app)
     , bookmark  = require('./bookmark')(crowi, app)
+    , revision  = require('./revision')(crowi, app)
     , loginRequired = middleware.loginRequired
     , loginRequired = middleware.loginRequired
     , accessTokenParser = middleware.accessTokenParser
     , accessTokenParser = middleware.accessTokenParser
     ;
     ;
@@ -91,6 +92,9 @@ module.exports = function(crowi, app) {
   app.post('/_api/likes.add'          , accessTokenParser(crowi, app) , loginRequired(crowi, app) , page.api.like);
   app.post('/_api/likes.add'          , accessTokenParser(crowi, app) , loginRequired(crowi, app) , page.api.like);
   app.post('/_api/likes.remove'       , accessTokenParser(crowi, app) , loginRequired(crowi, app) , page.api.unlike);
   app.post('/_api/likes.remove'       , accessTokenParser(crowi, app) , loginRequired(crowi, app) , page.api.unlike);
 
 
+  app.get( '/_api/revisions.get'      , accessTokenParser(crowi, app) , loginRequired(crowi, app) , revision.api.get);
+  app.get( '/_api/revisions.list'     , accessTokenParser(crowi, app) , loginRequired(crowi, app) ,revision.api.list);
+
   //app.get('/_api/revision/:id'     , user.useUserData()         , revision.api.get);
   //app.get('/_api/revision/:id'     , user.useUserData()         , revision.api.get);
   //app.get('/_api/r/:revisionId'    , user.useUserData()         , page.api.get);
   //app.get('/_api/r/:revisionId'    , user.useUserData()         , page.api.get);
 
 

+ 3 - 3
lib/routes/page.js

@@ -81,7 +81,7 @@ module.exports = function(crowi, app) {
     .then(function(portalPage) {
     .then(function(portalPage) {
       renderVars.page = portalPage;
       renderVars.page = portalPage;
 
 
-      return Page.findListByStartWith(path.substr(0, path.length -1), req.user, queryOptions);
+      return Page.findListByStartWith(path, req.user, queryOptions);
     }).then(function(pageList) {
     }).then(function(pageList) {
 
 
       if (pageList.length > limit) {
       if (pageList.length > limit) {
@@ -163,7 +163,7 @@ module.exports = function(crowi, app) {
           userData = data;
           userData = data;
           renderVars.pageUser = userData;
           renderVars.pageUser = userData;
 
 
-          return Bookmark.findByUser(userData, {limit: 10, populatePage: true});
+          return Bookmark.findByUser(userData, {limit: 10, populatePage: true, requestUser: req.user});
         }).then(function(bookmarkList) {
         }).then(function(bookmarkList) {
           debug(bookmarkList);
           debug(bookmarkList);
           renderVars.bookmarkList = bookmarkList;
           renderVars.bookmarkList = bookmarkList;
@@ -306,7 +306,7 @@ module.exports = function(crowi, app) {
     var renderVars = {};
     var renderVars = {};
 
 
     var pagerOptions = { offset: offset, limit : limit };
     var pagerOptions = { offset: offset, limit : limit };
-    var queryOptions = { offset: offset, limit : limit + 1, populatePage: true};
+    var queryOptions = { offset: offset, limit : limit + 1, populatePage: true, requestUser: req.user};
 
 
     User.findUserByUsername(username)
     User.findUserByUsername(username)
     .then(function(user) {
     .then(function(user) {

+ 52 - 0
lib/routes/revision.js

@@ -0,0 +1,52 @@
+module.exports = function(crowi, app) {
+  'use strict';
+
+  var debug = require('debug')('crowi:routes:revision')
+    , Revision = crowi.model('Revision')
+    , ApiResponse = require('../util/apiResponse')
+    , actions = {}
+  ;
+  actions.api = {};
+
+  /**
+   * @api {get} /revisions.get Get revision
+   * @apiName GetRevision
+   * @apiGroup Revision
+   *
+   * @apiParam {String} revision_id Revision Id.
+   */
+  actions.api.get = function(req, res) {
+    var revisionId = req.query.revision_id;
+
+    Revision
+      .findRevision(revisionId)
+      .then(function(revisionData) {
+        return res.json(ApiResponse.success(revisionData));
+      })
+      .catch(function(err) {
+        return res.json(ApiResponse.error(err));
+      });
+  };
+
+  /**
+   * @api {get} /revisions.list Get revisions
+   * @apiName ListRevision
+   * @apiGroup Revision
+   *
+   * @apiParam {String} revision_ids Revision Ids.
+   */
+  actions.api.list = function(req, res) {
+    var revisionIds = req.query.revision_ids.split(',');
+
+    Revision
+      .findRevisions(revisionIds)
+      .then(function(revisions) {
+        return res.json(ApiResponse.success(revisions));
+      })
+      .catch(function(err) {
+        return res.json(ApiResponse.error(err));
+      });
+  };
+
+  return actions;
+};

+ 5 - 3
lib/views/admin/app.html

@@ -3,9 +3,11 @@
 {% block html_title %}アプリ設定 · {% endblock %}
 {% block html_title %}アプリ設定 · {% endblock %}
 
 
 {% block content_head %}
 {% block content_head %}
-<header id="page-header">
-  <h1 class="title" id="">アプリ設定</h1>
-</header>
+<div class="header-wrap">
+  <header id="page-header">
+    <h1 class="title" id="">アプリ設定</h1>
+  </header>
+</div>
 {% endblock %}
 {% endblock %}
 
 
 {% block content_main %}
 {% block content_main %}

+ 5 - 3
lib/views/admin/index.html

@@ -3,9 +3,11 @@
 {% block html_title %}Wiki管理 · {{ path }}{% endblock %}
 {% block html_title %}Wiki管理 · {{ path }}{% endblock %}
 
 
 {% block content_head %}
 {% block content_head %}
-<header id="page-header">
-  <h1 class="title" id="">Wiki管理</h1>
-</header>
+<div class="header-wrap">
+  <header id="page-header">
+    <h1 class="title" id="">Wiki管理</h1>
+  </header>
+</div>
 {% endblock %}
 {% endblock %}
 
 
 {% block content_main %}
 {% block content_main %}

+ 5 - 3
lib/views/admin/users.html

@@ -3,9 +3,11 @@
 {% block html_title %}ユーザー管理 · {% endblock %}
 {% block html_title %}ユーザー管理 · {% endblock %}
 
 
 {% block content_head %}
 {% block content_head %}
-<header id="page-header">
-  <h1 class="title" id="">ユーザー管理</h1>
-</header>
+<div class="header-wrap">
+  <header id="page-header">
+    <h1 class="title" id="">ユーザー管理</h1>
+  </header>
+</div>
 {% endblock %}
 {% endblock %}
 
 
 {% block content_main %}
 {% block content_main %}

+ 4 - 37
lib/views/page.html

@@ -140,6 +140,10 @@
               {{ t.createdAt|datetz('Y-m-d H:i:s') }}
               {{ t.createdAt|datetz('Y-m-d H:i:s') }}
               <br>
               <br>
               <a href="?revision={{ t._id.toString() }}"><i class="fa fa-history"></i> このバージョンを見る</a>
               <a href="?revision={{ t._id.toString() }}"><i class="fa fa-history"></i> このバージョンを見る</a>
+              <a class="diff-view" data-revision-id="{{ t._id.toString() }}">
+                <i id="diff-icon-{{ t._id.toString() }}" class="fa fa-arrow-circle-right"></i> 差分を見る
+              </a>
+              <pre class="fk-hide" id="diff-display-{{ t._id.toString()}}"></pre>
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>
@@ -150,37 +154,6 @@
     </div>
     </div>
 
 
   </div>
   </div>
-  <script type="text/javascript">
-    $(function(){
-        var renderer = new Crowi.renderer($('#raw-text-original').html());
-        renderer.render();
-        Crowi.correctHeaders('#revision-body-content');
-        Crowi.revisionToc('#revision-body-content', '#revision-toc');
-
-        $('#edit-form').submit(function()
-        {
-          //console.log('save');
-          //return false;
-        });
-
-        //data-spy="affix" data-offset-top="80"
-        var headerHeight = $('#page-header').outerHeight(true);
-        $('.header-wrap').css({height: headerHeight + 'px'});
-        $('#page-header').affix({
-          offset: {
-            top: function() {
-              return headerHeight + 74; // (54 header + 20 padding-top)
-            }
-          }
-        });
-        $('[data-affix-disable]').on('click', function(e) {
-          $elm = $($(this).data('affix-disable'));
-          $(window).off('.affix');
-          $elm.removeData('affix').removeClass('affix affix-top affix-bottom');
-          return false;
-        });
-    });
-  </script>
   {% endif %}
   {% endif %}
 </div>
 </div>
 
 
@@ -218,12 +191,6 @@
 <div id="notifPageEdited" class="fk-hide fk-notif fk-notif-danger"><i class="fa fa-exclamation-triangle"></i> <span class="edited-user"></span>さんがこのページを編集しました。 <a href="javascript:location.reload();"><i class="fa fa-angle-double-right"></i> 最新版を読み込む</a></div>
 <div id="notifPageEdited" class="fk-hide fk-notif fk-notif-danger"><i class="fa fa-exclamation-triangle"></i> <span class="edited-user"></span>さんがこのページを編集しました。 <a href="javascript:location.reload();"><i class="fa fa-angle-double-right"></i> 最新版を読み込む</a></div>
 <div id="notifPageEditing" class="fk-hide fk-notif fk-notif-warning"><i class="fa fa-exclamation-triangle"></i> 他の人がこのページの編集を開始しました。</div>
 <div id="notifPageEditing" class="fk-hide fk-notif fk-notif-warning"><i class="fa fa-exclamation-triangle"></i> 他の人がこのページの編集を開始しました。</div>
 
 
-<div id="portal-warning-for-page" class="portal-warning-for-page alert alert-danger alert-dismissible" role="alert">
-  <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
-
-  <strong>Warning!</strong> /user/hoge のページが存在します。このページをポータル化するには、/ に移動し、「ページを移動」させてください。<br>
-/ とは別に ポータル を作成する場合、このまま編集を続けて作成してください。
-</div>
 
 
 <script>
 <script>
   $(function() {
   $(function() {

+ 1 - 1
lib/views/page_list.html

@@ -41,7 +41,7 @@
   data-page-is-seen="{% if page and page.isSeenUser(user) %}1{% else %}0{% endif %}"
   data-page-is-seen="{% if page and page.isSeenUser(user) %}1{% else %}0{% endif %}"
   >
   >
 
 
-<div class="portal {% if not page %}hide{% endif %}">
+<div class="portal {% if not page or req.query.offset > 0 %}hide{% endif %}">
 
 
   <ul class="nav nav-tabs hidden-print">
   <ul class="nav nav-tabs hidden-print">
    {# portal tab #}
    {# portal tab #}

+ 2 - 1
package.json

@@ -42,7 +42,8 @@
     "consolidate": "~0.11.0",
     "consolidate": "~0.11.0",
     "cookie-parser": "~1.3.4",
     "cookie-parser": "~1.3.4",
     "debug": "~2.2.0",
     "debug": "~2.2.0",
-    "elasticsearch": "~10.0.1",
+    "elasticsearch": "~11.0.1",
+    "diff": "~2.2.2",
     "errorhandler": "~1.3.4",
     "errorhandler": "~1.3.4",
     "express": "~4.13.3",
     "express": "~4.13.3",
     "express-form": "~0.12.0",
     "express-form": "~0.12.0",

+ 3 - 0
resource/css/_form.scss

@@ -1,4 +1,6 @@
 .crowi.main-container .main .content-main.on-edit { // {{{ Edit Form of Page
 .crowi.main-container .main .content-main.on-edit { // {{{ Edit Form of Page
+  padding: 0;
+
   position: fixed;
   position: fixed;
   z-index: 1060;
   z-index: 1060;
   background: #fff;
   background: #fff;
@@ -97,6 +99,7 @@
 } // }}}
 } // }}}
 
 
 .crowi.main-container .main .page-list.content-main { // {{{ Edit Form of Page List
 .crowi.main-container .main .page-list.content-main { // {{{ Edit Form of Page List
+
   .close-button {
   .close-button {
     display: none;
     display: none;
   }
   }

+ 12 - 1
resource/css/_layout.scss

@@ -61,6 +61,18 @@
 
 
     } // }}}
     } // }}}
 
 
+    .main {
+      padding: 0; // cancel bootstrap padding
+
+      .header-wrap {
+        padding: 16px 16px 0 16px;
+      }
+
+      .content-main {
+        padding: 16px;
+      }
+    }
+
     .layout-control { // {{{
     .layout-control { // {{{
       transition: .3s ease;
       transition: .3s ease;
       -webkit-transition: .3s ease;
       -webkit-transition: .3s ease;
@@ -249,7 +261,6 @@
     .main {
     .main {
       article header {
       article header {
         border-bottom: solid 1px #666;
         border-bottom: solid 1px #666;
-        margin-bottom: 20px;
         h1 {
         h1 {
           font-size: 2em;
           font-size: 2em;
           color: #000;
           color: #000;

+ 3 - 3
resource/css/_page.scss

@@ -1,11 +1,11 @@
 .crowi.main-container {
 .crowi.main-container {
+  // padding controll of .header-wrap and .content-main are moved to _layout and _form
 
 
   .main { // {{{ .main of layout related
   .main { // {{{ .main of layout related
     transition: .5s ease;
     transition: .5s ease;
     -webkit-transition: .5s ease;
     -webkit-transition: .5s ease;
     background: #fff;
     background: #fff;
 
 
-    padding: 20px;
     article {
     article {
       background: #fff;
       background: #fff;
     }
     }
@@ -65,6 +65,7 @@
       h1 {
       h1 {
         font-size: 28px;
         font-size: 28px;
         margin-top: 0;
         margin-top: 0;
+        margin-bottom: 0;
 
 
         a:last-child {
         a:last-child {
           color: #D1E2E4;
           color: #D1E2E4;
@@ -89,7 +90,7 @@
   .main.grant-specified,
   .main.grant-specified,
   .main.grant-owner {
   .main.grant-owner {
     background: #333;
     background: #333;
-    padding: 20px 10px;
+    padding: 16px;
 
 
     .page-grant {
     .page-grant {
       color: #ccc;
       color: #ccc;
@@ -97,7 +98,6 @@
 
 
     article {
     article {
       border-radius: 5px;
       border-radius: 5px;
-      padding: 20px;
     }
     }
   }
   }
   // }}}
   // }}}

+ 0 - 8
resource/css/_user.scss

@@ -1,9 +1,7 @@
 .crowi.main-container {
 .crowi.main-container {
   .main.user-page { // {{{ .main of layout related
   .main.user-page { // {{{ .main of layout related
-    padding: 0;
 
 
     .header-wrap {
     .header-wrap {
-      padding: 20px;
 
 
       h1 {
       h1 {
         margin: 0;
         margin: 0;
@@ -58,11 +56,5 @@
       }
       }
 
 
     }
     }
-    .content-main {
-      padding: 20px;
-      &.on-edit {
-        padding: 0;
-      }
-    }
   } // }}}
   } // }}}
 }
 }

+ 0 - 6
resource/css/crowi.scss

@@ -62,12 +62,6 @@ footer, aside {
   margin-bottom: 1em;
   margin-bottom: 1em;
 }
 }
 
 
-article {
-  header {
-    margin-bottom: 20px;
-  }
-}
-
 footer {
 footer {
   h4,
   h4,
   h3 {
   h3 {

+ 31 - 1
resource/js/crowi-form.js

@@ -128,7 +128,7 @@ $(function() {
         var indent = listMarkMatch[1];
         var indent = listMarkMatch[1];
         var num = parseInt(listMarkMatch[2]);
         var num = parseInt(listMarkMatch[2]);
         if (num !== 1) {
         if (num !== 1) {
-          listMark = listMark.return(/\s*\d+/, indent + (num +1));
+          listMark = listMark.replace(/\s*\d+/, indent + (num +1));
         }
         }
       }
       }
       $target.selection('insert', {text: "\n" + listMark, mode: 'before'});
       $target.selection('insert', {text: "\n" + listMark, mode: 'before'});
@@ -212,6 +212,36 @@ $(function() {
     }
     }
   });
   });
 
 
+  var handlePasteEvent = function(event) {
+    var currentLine = getCurrentLine(event);
+
+    if (!currentLine) {
+      return false;
+    }
+    var $target = $(event.target);
+    var pasteText = event.clipboardData.getData('text');
+
+    var match = currentLine.text.match(/^(\s*(?:>|\-|\+|\*|\d+\.) (?:\[(?:x| )\] )?)/);
+    if (match) {
+      if (pasteText.match(/(?:\r\n|\r|\n)/)) {
+        pasteText = pasteText.replace(/(\r\n|\r|\n)/g, "$1" + match[1]);
+      }
+    }
+
+    $target.selection('insert', {text: pasteText, mode: 'after'});
+
+    var newPos = currentLine.end + pasteText.length;
+    $target.selection('setPos', {start: newPos, end: newPos});
+
+    return true;
+  };
+
+  document.getElementById('form-body').addEventListener('paste', function(event) {
+    if (handlePasteEvent(event)) {
+      event.preventDefault();
+    }
+  });
+
   var unbindInlineAttachment = function($form) {
   var unbindInlineAttachment = function($form) {
     $form.unbind('.inlineattach');
     $form.unbind('.inlineattach');
   };
   };

+ 105 - 2
resource/js/crowi.js

@@ -4,6 +4,7 @@
 */
 */
 
 
 var hljs = require('highlight.js');
 var hljs = require('highlight.js');
+var jsdiff = require('diff');
 var marked = require('marked');
 var marked = require('marked');
 var Crowi = {};
 var Crowi = {};
 
 
@@ -321,6 +322,35 @@ $(function() {
 
 
   if (pageId) {
   if (pageId) {
 
 
+    // if page exists
+    var $rawTextOriginal = $('#raw-text-original');
+    if ($rawTextOriginal.length > 0) {
+      var renderer = new Crowi.renderer($('#raw-text-original').html());
+      renderer.render();
+      Crowi.correctHeaders('#revision-body-content');
+      Crowi.revisionToc('#revision-body-content', '#revision-toc');
+    }
+
+    // header
+    var $header = $('#page-header');
+    if ($header.length > 0) {
+      var headerHeight = $header.outerHeight(true);
+      $('.header-wrap').css({height: (headerHeight + 16) + 'px'});
+      $header.affix({
+        offset: {
+          top: function() {
+            return headerHeight + 86; // (54 header + 16 header padding-top + 16 content padding-top)
+          }
+        }
+      });
+      $('[data-affix-disable]').on('click', function(e) {
+        $elm = $($(this).data('affix-disable'));
+        $(window).off('.affix');
+        $elm.removeData('affix').removeClass('affix affix-top affix-bottom');
+        return false;
+      });
+    }
+
     // omg
     // omg
     function createCommentHTML(revision, creator, comment, commentedAt) {
     function createCommentHTML(revision, creator, comment, commentedAt) {
       var $comment = $('<div>');
       var $comment = $('<div>');
@@ -549,7 +579,8 @@ $(function() {
 
 
     var $seenUserList = $("#seen-user-list");
     var $seenUserList = $("#seen-user-list");
     var seenUsers = $seenUserList.data('seen-users');
     var seenUsers = $seenUserList.data('seen-users');
-    if (seenUsers && seenUsers.length > 0 && seenUsers.length <= 10) {
+    var seenUsersArray = seenUsers.split(',');
+    if (seenUsers && seenUsersArray.length > 0 && seenUsersArray.length <= 10) {
       // FIXME: user data cache
       // FIXME: user data cache
       $.get('/_api/users.list', {user_ids: seenUsers}, function(res) {
       $.get('/_api/users.list', {user_ids: seenUsers}, function(res) {
         // ignore unless response has error
         // ignore unless response has error
@@ -578,6 +609,78 @@ $(function() {
         $seenUserList.append(CreateUserLinkWithPicture(user));
         $seenUserList.append(CreateUserLinkWithPicture(user));
       });
       });
     }
     }
+
+    // History Diff
+    var allRevisionIds = [];
+    $.each($('.diff-view'), function() {
+      allRevisionIds.push($(this).data('revisionId'));
+    });
+
+    $('.diff-view').on('click', function(e) {
+      e.preventDefault();
+
+      var getBeforeRevisionId = function(revisionId) {
+        var currentPos = $.inArray(revisionId, allRevisionIds);
+        if (currentPos < 0) {
+          return false;
+        }
+
+        var beforeRevisionId = allRevisionIds[currentPos + 1];
+        if (typeof beforeRevisionId === 'undefined') {
+          return false;
+        }
+
+        return beforeRevisionId;
+      };
+
+      var revisionId = $(this).data('revisionId');
+      var beforeRevisionId = getBeforeRevisionId(revisionId);
+      var $diffDisplay = $('#diff-display-' + revisionId);
+      var $diffIcon = $('#diff-icon-' + revisionId);
+
+      if ($diffIcon.hasClass('fa-arrow-circle-right')) {
+        $diffIcon.removeClass('fa-arrow-circle-right');
+        $diffIcon.addClass('fa-arrow-circle-down');
+      } else {
+        $diffIcon.removeClass('fa-arrow-circle-down');
+        $diffIcon.addClass('fa-arrow-circle-right');
+      }
+
+      if (beforeRevisionId === false) {
+        $diffDisplay.text('差分はありません');
+        $diffDisplay.slideToggle();
+      } else {
+        var revisionIds = revisionId + ',' + beforeRevisionId;
+
+        $.ajax({
+          type: 'GET',
+          url: '/_api/revisions.list?revision_ids=' + revisionIds,
+          dataType: 'json'
+        }).done(function(res) {
+          var currentText = res[0].body;
+          var previousText = res[1].body;
+
+          $diffDisplay.text('');
+
+          var diff = jsdiff.diffLines(previousText, currentText);
+          diff.forEach(function(part) {
+            var color = part.added ? 'green' : part.removed ? 'red' : 'grey';
+            var $span = $('<span>');
+            $span.css('color', color);
+            $span.text(part.value);
+            $diffDisplay.append($span);
+          });
+
+          $diffDisplay.slideToggle();
+        });
+      }
+    });
+
+    // default open
+    $('.diff-view').each(function(i, diffView) {
+      if (i < 2) {
+        $(diffView).click();
+      }
+    });
   }
   }
 });
 });
-