Bläddra i källkod

Experiment new ergonomics to interact with card details

The idea is that by displaying card details in a sidebar stuck on the
right of the screen, the mouse had to travel too much before
interacting with it. I also don’t want to use the Trello solution
(modal) on big screens, because I like the ability to interact with
the selected card and with the board at the same time (like in a
e-mail client).

The solution introduced in this commit consist of opening the card
detail in a column next to the minicard list.

This commit also fix right sidebar members and labels drag and drop.
Maxime Quandalle 10 år sedan
förälder
incheckning
781577db04

+ 0 - 5
.jscsrc

@@ -66,11 +66,6 @@
         "catch",
         "typeof"
     ],
-    "safeContextKeyword": [
-        "self",
-        "context",
-        "view"
-    ],
     "validateLineBreaks": "LF",
     "validateQuoteMarks": "'",
     "validateIndentation": 2,

+ 0 - 1
.jshintrc

@@ -75,7 +75,6 @@
     // XXX Temp, we should remove these
     "allowIsBoardAdmin": true,
     "allowIsBoardMember": true,
-    "BoardSubsManager": true,
     "currentlyOpenedForm": true,
     "Emoji": true
   }

+ 3 - 2
client/components/boards/body.jade

@@ -1,6 +1,7 @@
 //-
   XXX This template can't be transformed into a component because it is
   included by iron-router. That's a bug.
+  See https://github.com/peerlibrary/meteor-blaze-components/issues/44
 template(name="board")
   +boardComponent
 
@@ -11,11 +12,11 @@ template(name="boardComponent")
         .lists.js-lists
           each lists
             +list(this)
+            if currentCardIsInThisList
+              +cardDetails(currentCard)
           if currentUser.isBoardMember
             +addListForm
       +boardSidebar
-    if currentCard
-      +cardSidebar(currentCard)
   else
     +message(label="board-no-found")
 

+ 68 - 31
client/components/boards/body.js

@@ -1,3 +1,12 @@
+// XXX This event list must be abstracted somewhere else.
+var endTransitionEvents = [
+  'webkitTransitionEnd',
+  'otransitionend',
+  'oTransitionEnd',
+  'msTransitionEnd',
+  'transitionend'
+].join(' ');
+
 BlazeComponent.extendComponent({
   template: function() {
     return 'boardComponent';
@@ -17,50 +26,78 @@ BlazeComponent.extendComponent({
     // TODO
   },
 
+  currentCardIsInThisList: function() {
+    var currentCard = Cards.findOne(Session.get('currentCard'));
+    var listId = this.currentData()._id;
+    return currentCard && currentCard.listId === listId;
+  },
+
   onRendered: function() {
     var self = this;
 
     self.scrollLeft();
 
-    if (Meteor.user().isBoardMember()) {
-      self.$('.js-lists').sortable({
-        tolerance: 'pointer',
-        appendTo: '.js-lists',
-        helper: 'clone',
-        items: '.js-list:not(.add-list)',
-        placeholder: 'list placeholder',
-        start: function(event, ui) {
-          $('.list.placeholder').height(ui.item.height());
-          Popup.close();
-        },
-        stop: function() {
-          self.$('.js-lists').find('.js-list:not(.add-list)').each(
-            function(i, list) {
-              var data = Blaze.getData(list);
-              Lists.update(data._id, {
-                $set: {
-                  sort: i
-                }
-              });
-            }
-          );
+    var lists = this.find('.js-lists');
+
+    // We want to animate the card details window closing. We rely on CSS
+    // transition for the actual animation.
+    lists._uihooks = {
+      removeElement: function(node) {
+        var removeNode = function() {
+          node.parentNode.removeChild(node);
+        };
+        if ($(node).hasClass('js-card-detail')) {
+          $(node).css({
+            flex: '0',
+            padding: 0
+          });
+          $(lists).one(endTransitionEvents, function() {
+            removeNode();
+          });
+        } else {
+          removeNode();
         }
-      });
+      }
+    };
+
+    if (! Meteor.user().isBoardMember())
+      return;
 
-      // If there is no data in the board (ie, no lists) we autofocus the list
-      // creation form by clicking on the corresponding element.
-      if (self.data().lists().count() === 0) {
-        this.openNewListForm();
+    self.$(lists).sortable({
+      tolerance: 'pointer',
+      appendTo: '.js-lists',
+      helper: 'clone',
+      items: '.js-list:not(.add-list)',
+      placeholder: 'list placeholder',
+      start: function(event, ui) {
+        $('.list.placeholder').height(ui.item.height());
+        Popup.close();
+      },
+      stop: function() {
+        self.$('.js-lists').find('.js-list:not(.add-list)').each(
+          function(i, list) {
+            var data = Blaze.getData(list);
+            Lists.update(data._id, {
+              $set: {
+                sort: i
+              }
+            });
+          }
+        );
       }
+    });
+
+    // If there is no data in the board (ie, no lists) we autofocus the list
+    // creation form by clicking on the corresponding element.
+    if (self.data().lists().count() === 0) {
+      this.openNewListForm();
     }
   },
 
   sidebarSize: function() {
     var sidebar = this.componentChildren('boardSidebar')[0];
-    if (Session.get('currentCard') !== null)
-      return 'next-large-sidebar';
-    else if (sidebar && sidebar.isOpen())
-      return 'next-small-sidebar';
+    if (sidebar && sidebar.isOpen())
+      return 'next-sidebar';
   }
 }).register('boardComponent');
 

+ 1 - 5
client/components/boards/body.styl

@@ -16,13 +16,9 @@
     bottom: 0
     transition: margin .1s
 
-    &.next-small-sidebar
+    &.next-sidebar
       margin-right: 248px
 
-    &.next-large-sidebar
-      opacity: 0.8
-      margin-right: 496px
-
 .lists
   align-items: flex-start
   display: flex

+ 1 - 1
client/components/boards/colors.styl

@@ -10,7 +10,7 @@ setBoardColor(color)
     background-color: color
 
   & .minicard.is-selected .minicard-details
-    border-bottom: 2px solid color
+    border-left: 3px solid color
 
   button[type=submit].primary, input[type=submit].primary
     background-color: darken(color, 20%)

+ 25 - 2
client/components/boards/router.js

@@ -1,6 +1,6 @@
 Meteor.subscribe('boards');
 
-BoardSubsManager = new SubsManager();
+var boardSubsManager = new SubsManager();
 
 Router.route('/boards', {
   name: 'Boards',
@@ -17,6 +17,7 @@ Router.route('/boards/:_id/:slug', {
   name: 'Board',
   template: 'board',
   onAfterAction: function() {
+    // XXX We probably shouldn't rely on Session
     Session.set('sidebarIsOpen', true);
     Session.set('currentWidget', 'home');
     Session.set('menuWidgetIsOpen', false);
@@ -26,9 +27,31 @@ Router.route('/boards/:_id/:slug', {
     Session.set('currentBoard', params._id);
     Session.set('currentCard', null);
 
-    return BoardSubsManager.subscribe('board', params._id, params.slug);
+    return boardSubsManager.subscribe('board', params._id, params.slug);
   },
   data: function() {
     return Boards.findOne(this.params._id);
   }
 });
+
+Router.route('/boards/:boardId/:slug/:cardId', {
+  name: 'Card',
+  template: 'board',
+  onAfterAction: function() {
+    Tracker.nonreactive(function() {
+      if (! Session.get('currentCard') && typeof Sidebar !== 'undefined') {
+        Sidebar.hide();
+      }
+    });
+    var params = this.params;
+    Session.set('currentBoard', params.boardId);
+    Session.set('currentCard', params.cardId);
+  },
+  waitOn: function() {
+    var params = this.params;
+    return boardSubsManager.subscribe('board', params.boardId, params.slug);
+  },
+  data: function() {
+    return Boards.findOne(this.params.boardId);
+  }
+});

+ 43 - 44
client/components/cards/details.jade

@@ -1,47 +1,46 @@
-template(name="cardSidebar")
-  .card-sidebar.sidebar
-    .card-detail.sidebar-content.js-card-sidebar-content
-      if cover
-        .card-detail-cover(style="background-image: url({{ card.cover.url }})")
-      .card-detail-header(class="{{#if currentUser.isBoardMember}}editable{{/if}}")
-        a.js-close-card-detail
-          i.fa.fa-times
-        h2.card-detail-title.js-card-title= title
-      p.card-detail-list.js-move-card
-        | {{_ 'in-list'}}
-        a.card-detail-list-title(
-          class="{{#if currentUser.isBoardMember}}js-open-move-from-header is-editable{{/if}}")
-          = list.title
-      hr
-      //- if card.members
-      .card-detail-item.card-detail-item-members.clearfix.js-card-detail-members
-        h3.card-detail-item-header {{_ 'members'}}
-        .js-card-detail-members-list.clearfix
-          each members
-            +userAvatar(userId=this size="small" cardId=../_id)
-          a.card-detail-item-add-button.dark-hover.js-details-edit-members
-            i.fa.fa-plus
-      //- We should use "editable" to avoide repetiting ourselves
-      .clearfix
-      if currentUser.isBoardMember
-        h3 Description
-        +inlinedForm(classNames="js-card-description")
-          i.fa.fa-times.js-close-inlined-form
-          textarea(autofocus)= description
-          button(type="submit") {{_ 'edit'}}
-        else
-          .js-open-inlined-form
-            a {{_ 'edit'}}
-            +viewer
-              = description
-      else if description
-        h3 Description
-        +viewer
-          = description
-      hr
-      if attachments.count
-        +WindowAttachmentsModule(card=this)
-      +WindowActivityModule(card=this)
+template(name="cardDetails")
+  .card-detail.js-card-detail: .card-detail-canvas
+    if cover
+      .card-detail-cover(style="background-image: url({{ card.cover.url }})")
+    .card-detail-header(class="{{#if currentUser.isBoardMember}}editable{{/if}}")
+      a.js-close-card-detail
+        i.fa.fa-times
+      h2.card-detail-title.js-card-title= title
+    p.card-detail-list.js-move-card
+      | {{_ 'in-list'}}
+      a.card-detail-list-title(
+        class="{{#if currentUser.isBoardMember}}js-open-move-from-header is-editable{{/if}}")
+        = list.title
+    hr
+    //- if card.members
+    .card-detail-item.card-detail-item-members.clearfix.js-card-detail-members
+      h3.card-detail-item-header {{_ 'members'}}
+      .js-card-detail-members-list.clearfix
+        each members
+          +userAvatar(userId=this size="small" cardId=../_id)
+        a.card-detail-item-add-button.dark-hover.js-details-edit-members
+          i.fa.fa-plus
+    //- We should use "editable" to avoide repetiting ourselves
+    .clearfix
+    if currentUser.isBoardMember
+      h3 Description
+      +inlinedForm(classNames="js-card-description")
+        i.fa.fa-times.js-close-inlined-form
+        textarea(autofocus)= description
+        button(type="submit") {{_ 'edit'}}
+      else
+        .js-open-inlined-form
+          a {{_ 'edit'}}
+          +viewer
+            = description
+    else if description
+      h3 Description
+      +viewer
+        = description
+    hr
+    if attachments.count
+      +WindowAttachmentsModule(card=this)
+    +WindowActivityModule(card=this)
 
 template(name="moveCardPopup")
   +boardLists

+ 3 - 3
client/components/cards/details.js

@@ -1,6 +1,6 @@
 BlazeComponent.extendComponent({
   template: function() {
-    return 'cardSidebar';
+    return 'cardDetails';
   },
 
   mixins: function() {
@@ -8,7 +8,7 @@ BlazeComponent.extendComponent({
   },
 
   calculateNextPeak: function() {
-    var altitude = this.find('.js-card-sidebar-content').scrollHeight;
+    var altitude = this.find('.js-card-detail').scrollHeight;
     this.callFirstWith(this, 'setNextPeak', altitude);
   },
 
@@ -86,7 +86,7 @@ BlazeComponent.extendComponent({
       'click .js-details-edit-labels': Popup.open('cardLabels')
     }];
   }
-}).register('cardSidebar');
+}).register('cardDetails');
 
 Template.moveCardPopup.events({
   'click .js-select-list': function() {

+ 52 - 40
client/components/cards/details.styl

@@ -1,45 +1,57 @@
 @import 'nib'
 
-.card-sidebar.sidebar
-  width: 496px
-  top: -46px
-
-  .card-detail.sidebar-content
-    padding: 0 20px
-    z-index: 20 !important
-    // XXX Animate apparition
-
-    .card-detail-header
-      margin: 0 -20px 5px
-      padding 7px 20px 0
-      background: #F7F7F7
-      border-bottom: 1px solid darken(white, 10%)
-      min-height: 38px
-
-      i.fa
-        float: right
-        font-size: 1.3em
-        color: darken(white, 35%)
-        margin-top: 7px
-
-      .card-detail-title
-        font-weight: bold
-        font-size: 1.7em
-        margin: 3px 0 0
-        padding: 0
-
-    .card-detail-list
-      font-size: 0.85em
-      margin-bottom: 3px
-
-      a.card-detail-list-title
-        font-weight: bold
-
-        &.is-editable
-          display: inline-block
-          background: darken(white, 10%)
-          border-radius: 3px
-          padding: 0px 5px
+.card-detail
+  padding: 0 20px
+  height: 100%
+  flex: 0 0 470px
+  overflow: hidden
+  background: white
+  border-radius: 3px
+  z-index: 20 !important
+  animation: growIn 0.2s
+  box-shadow: 0 0 7px 0 darken(white, 30%)
+  transition: flex 0.2s, padding 0.2s
+
+  .card-detail-canvas
+    width: 470px
+
+  .card-detail-header
+    margin: 0 -20px 5px
+    padding 7px 20px 0
+    background: #F7F7F7
+    border-bottom: 1px solid darken(white, 10%)
+    min-height: 38px
+
+    i.fa
+      float: right
+      font-size: 1.3em
+      color: darken(white, 35%)
+      margin-top: 7px
+
+    .card-detail-title
+      font-weight: bold
+      font-size: 1.7em
+      margin: 3px 0 0
+      padding: 0
+
+  .card-detail-list
+    font-size: 0.85em
+    margin-bottom: 3px
+
+    a.card-detail-list-title
+      font-weight: bold
+
+      &.is-editable
+        display: inline-block
+        background: darken(white, 10%)
+        border-radius: 3px
+        padding: 0px 5px
+
+@keyframes growIn
+  from
+    flex: 0 0 0
+  to
+    flex: 0 0 470px
 
 .new-comment
   position: relative

+ 8 - 3
client/components/cards/minicard.styl

@@ -4,7 +4,6 @@
   border-radius: 2px
   cursor: pointer
   margin-bottom: 9px
-  max-width: 300px
   min-height: 20px
   position: relative
   z-index: 0
@@ -42,10 +41,16 @@
     position: relative
     z-index: 10
 
-
   &.is-selected
+    margin-left: -11px
+    transform: translateX(- @margin-left)
+    border-bottom-right-radius: 0
+    border-top-right-radius: 0
+    z-index: 100
+    box-shadow: -2px 1px 2px rgba(0,0,0,.2)
+
     .minicard-details
-      padding-bottom: 0
+      margin-right: 11px
 
   a.minicard-details
     text-decoration:none

+ 0 - 15
client/components/cards/router.js

@@ -1,15 +0,0 @@
-Router.route('/boards/:boardId/:slug/:cardId', {
-  name: 'Card',
-  template: 'board',
-  waitOn: function() {
-    var params = this.params;
-    // XXX We probably shouldn't rely on Session
-    Session.set('currentBoard', params.boardId);
-    Session.set('currentCard', params.cardId);
-
-    return BoardSubsManager.subscribe('board', params.boardId, params.slug);
-  },
-  data: function() {
-    return Boards.findOne(this.params.boardId);
-  }
-});

+ 1 - 1
client/components/lists/body.jade

@@ -4,7 +4,7 @@ template(name="listBody")
       +inlinedForm(autoclose=false position="top")
         +addCardForm(listId=_id position="top")
     each cards
-      .minicard.card.js-minicard.js-member-droppable(
+      .minicard.card.js-minicard(
         class="{{#if isSelected}}is-selected{{/if}}")
         a.minicard-details.clearfix.show(href=absoluteUrl)
           if cover

+ 12 - 15
client/components/lists/main.js

@@ -25,13 +25,14 @@ BlazeComponent.extendComponent({
   onRendered: function() {
     if (Meteor.user().isBoardMember()) {
       var boardComponent = this.componentParent();
+      var itemsSelector = '.js-minicard:not(.placeholder, .hide, .js-composer)';
       var $cards = this.$('.js-minicards');
       $cards.sortable({
         connectWith: '.js-minicards',
         tolerance: 'pointer',
         appendTo: '.js-lists',
         helper: 'clone',
-        items: '.js-minicard:not(.placeholder, .hide, .js-composer)',
+        items: itemsSelector,
         placeholder: 'minicard placeholder',
         start: function(event, ui) {
           $('.minicard.placeholder').height(ui.item.height());
@@ -57,24 +58,20 @@ BlazeComponent.extendComponent({
         }
       }).disableSelection();
 
-      Utils.liveEvent('mouseover', function($el) {
-        $el.find('.js-member-droppable').droppable({
+      $(document).on('mouseover', function() {
+        $cards.find(itemsSelector).droppable({
           hoverClass: 'draggable-hover-card',
-          accept: '.js-member',
+          accept: '.js-member,.js-label',
           drop: function(event, ui) {
-            var memberId = Blaze.getData(ui.draggable.get(0)).userId;
             var cardId = Blaze.getData(this)._id;
-            Cards.update(cardId, {$addToSet: {members: memberId}});
-          }
-        });
 
-        $el.find('.js-member-droppable').droppable({
-          hoverClass: 'draggable-hover-card',
-          accept: '.js-label',
-          drop: function(event, ui) {
-            var labelId = Blaze.getData(ui.draggable.get(0))._id;
-            var cardId = Blaze.getData(this)._id;
-            Cards.update(cardId, {$addToSet: {labelIds: labelId}});
+            if (ui.draggable.hasClass('js-member')) {
+              var memberId = Blaze.getData(ui.draggable.get(0)).userId;
+              Cards.update(cardId, {$addToSet: {members: memberId}});
+            } else {
+              var labelId = Blaze.getData(ui.draggable.get(0))._id;
+              Cards.update(cardId, {$addToSet: {labelIds: labelId}});
+            }
           }
         });
       });

+ 3 - 9
client/components/lists/main.styl

@@ -10,8 +10,7 @@
   // transparent, because that won't work during a list drag.
   background: darken(white, 10%)
   height: 100%
-  border-right: 1px solid darken(white, 17%)
-  border-left: 1px solid darken(white, 4%)
+  border-left: 1px solid darken(white, 20%)
   padding: 12px 7px 5px
   overflow-y: auto
 
@@ -19,9 +18,8 @@
     margin-left: 5px
     border-left: none
 
-  &:last-child
-    margin-right: 5px
-    border-right: none
+  .card-detail + &
+    border-left: none
 
   &.editable
     cursor: grab
@@ -87,9 +85,6 @@
     margin: 0
 
 .minicards
-  // flex: 1 1 auto
-  overflow-y: auto
-  overflow-x: hidden
   padding: 4px 4px 1px
   z-index: 1
   height: 100%
@@ -105,7 +100,6 @@
   padding: 7px 10px
   position: relative
   text-decoration: none
-  animation: fadeIn 0.2s
 
   i.fa
     margin-right: 7px

+ 1 - 1
client/components/main/popup.js

@@ -1,4 +1,4 @@
-// XXX This event list should be abstracted somewhere else.
+// XXX This event list must be abstracted somewhere else.
 var endTransitionEvents = [
   'webkitTransitionEnd',
   'otransitionend',

+ 5 - 5
client/components/sidebar/rendered.js

@@ -1,10 +1,11 @@
-Template.membersWidget.rendered = function() {
+Template.membersWidget.onRendered(function() {
+  var self = this;
   if (! Meteor.user().isBoardMember())
     return;
 
   _.each(['.js-member', '.js-label'], function(className) {
-    Utils.liveEvent('mouseover', function($this) {
-      $this.find(className).draggable({
+    $(document).on('mouseover', function() {
+      self.$(className).draggable({
         appendTo: 'body',
         helper: 'clone',
         revert: 'invalid',
@@ -17,5 +18,4 @@ Template.membersWidget.rendered = function() {
       });
     });
   });
-};
-
+});

+ 2 - 1
client/components/sidebar/sidebar.js

@@ -8,7 +8,8 @@ BlazeComponent.extendComponent({
   },
 
   onCreated: function() {
-    this._isOpen = new ReactiveVar(true);
+    this._isOpen = new ReactiveVar(! Session.get('currentCard'));
+    Sidebar = this;
   },
 
   isOpen: function() {

+ 0 - 1
client/config/router.js

@@ -19,7 +19,6 @@ Router.configure({
 
     // Reset default sessions
     Session.set('error', false);
-    Session.set('warning', false);
 
     Popup.close();
 

+ 1 - 6
client/lib/keyboard.js

@@ -19,12 +19,7 @@ Mousetrap.bind('esc', function() {
 });
 
 Mousetrap.bind('w', function() {
-  if (! Session.get('currentCard')) {
-    Sidebar.toogle();
-  } else {
-    Utils.goBoardId(Session.get('currentBoard'));
-    Sidebar.hide();
-  }
+  Sidebar.toogle();
 });
 
 Mousetrap.bind('q', function() {

+ 0 - 18
client/lib/utils.js

@@ -18,18 +18,6 @@ Utils = {
     };
   },
 
-  Warning: {
-    get: function() {
-      return Session.get('warning');
-    },
-    open: function(desc) {
-      Session.set('warning', { desc: desc });
-    },
-    close: function() {
-      Session.set('warning', false);
-    }
-  },
-
   // XXX We should remove these two methods
   goBoardId: function(_id) {
     var board = Boards.findOne(_id);
@@ -49,12 +37,6 @@ Utils = {
     });
   },
 
-  liveEvent: function(events, callback) {
-    $(document).on(events, function() {
-      callback($(this));
-    });
-  },
-
   capitalize: function(string) {
     return string.charAt(0).toUpperCase() + string.slice(1);
   },

+ 2 - 0
client/styles/main.styl

@@ -229,6 +229,8 @@ dd
   font-weight: 700
   line-height: 18px
 
+.ui-draggable-dragging
+  z-index: 200
 
 .board-backgrounds-list