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.
This commit is contained in:
Maxime Quandalle 2015-05-24 12:30:58 +02:00
parent 40b605f7d8
commit 781577db04
22 changed files with 225 additions and 203 deletions

View file

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

View file

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

View file

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

View file

@ -1,3 +1,12 @@
// XXX This event list must be abstracted somewhere else.
var endTransitionEvents = [
'webkitTransitionEnd',
'otransitionend',
'oTransitionEnd',
'msTransitionEnd',
'transitionend'
].join(' ');
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
template: function() { template: function() {
return 'boardComponent'; return 'boardComponent';
@ -17,50 +26,78 @@ BlazeComponent.extendComponent({
// TODO // TODO
}, },
currentCardIsInThisList: function() {
var currentCard = Cards.findOne(Session.get('currentCard'));
var listId = this.currentData()._id;
return currentCard && currentCard.listId === listId;
},
onRendered: function() { onRendered: function() {
var self = this; var self = this;
self.scrollLeft(); self.scrollLeft();
if (Meteor.user().isBoardMember()) { var lists = this.find('.js-lists');
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
}
});
}
);
}
});
// If there is no data in the board (ie, no lists) we autofocus the list // We want to animate the card details window closing. We rely on CSS
// creation form by clicking on the corresponding element. // transition for the actual animation.
if (self.data().lists().count() === 0) { lists._uihooks = {
this.openNewListForm(); 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;
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() { sidebarSize: function() {
var sidebar = this.componentChildren('boardSidebar')[0]; var sidebar = this.componentChildren('boardSidebar')[0];
if (Session.get('currentCard') !== null) if (sidebar && sidebar.isOpen())
return 'next-large-sidebar'; return 'next-sidebar';
else if (sidebar && sidebar.isOpen())
return 'next-small-sidebar';
} }
}).register('boardComponent'); }).register('boardComponent');

View file

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

View file

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

View file

@ -1,6 +1,6 @@
Meteor.subscribe('boards'); Meteor.subscribe('boards');
BoardSubsManager = new SubsManager(); var boardSubsManager = new SubsManager();
Router.route('/boards', { Router.route('/boards', {
name: 'Boards', name: 'Boards',
@ -17,6 +17,7 @@ Router.route('/boards/:_id/:slug', {
name: 'Board', name: 'Board',
template: 'board', template: 'board',
onAfterAction: function() { onAfterAction: function() {
// XXX We probably shouldn't rely on Session
Session.set('sidebarIsOpen', true); Session.set('sidebarIsOpen', true);
Session.set('currentWidget', 'home'); Session.set('currentWidget', 'home');
Session.set('menuWidgetIsOpen', false); Session.set('menuWidgetIsOpen', false);
@ -26,9 +27,31 @@ Router.route('/boards/:_id/:slug', {
Session.set('currentBoard', params._id); Session.set('currentBoard', params._id);
Session.set('currentCard', null); Session.set('currentCard', null);
return BoardSubsManager.subscribe('board', params._id, params.slug); return boardSubsManager.subscribe('board', params._id, params.slug);
}, },
data: function() { data: function() {
return Boards.findOne(this.params._id); 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);
}
});

View file

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

View file

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

View file

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

View file

@ -4,7 +4,6 @@
border-radius: 2px border-radius: 2px
cursor: pointer cursor: pointer
margin-bottom: 9px margin-bottom: 9px
max-width: 300px
min-height: 20px min-height: 20px
position: relative position: relative
z-index: 0 z-index: 0
@ -42,10 +41,16 @@
position: relative position: relative
z-index: 10 z-index: 10
&.is-selected &.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 .minicard-details
padding-bottom: 0 margin-right: 11px
a.minicard-details a.minicard-details
text-decoration:none text-decoration:none

View file

@ -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);
}
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 // XXX We should remove these two methods
goBoardId: function(_id) { goBoardId: function(_id) {
var board = Boards.findOne(_id); var board = Boards.findOne(_id);
@ -49,12 +37,6 @@ Utils = {
}); });
}, },
liveEvent: function(events, callback) {
$(document).on(events, function() {
callback($(this));
});
},
capitalize: function(string) { capitalize: function(string) {
return string.charAt(0).toUpperCase() + string.slice(1); return string.charAt(0).toUpperCase() + string.slice(1);
}, },

View file

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