From 781577db041e0008de22f31bcc1cb11ae96670e0 Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Sun, 24 May 2015 12:30:58 +0200 Subject: Experiment new ergonomics to interact with card details MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .jscsrc | 5 -- .jshintrc | 1 - client/components/boards/body.jade | 5 +- client/components/boards/body.js | 99 ++++++++++++++++++++++++----------- client/components/boards/body.styl | 6 +-- client/components/boards/colors.styl | 2 +- client/components/boards/router.js | 27 +++++++++- client/components/cards/details.jade | 87 +++++++++++++++--------------- client/components/cards/details.js | 6 +-- client/components/cards/details.styl | 92 ++++++++++++++++++-------------- client/components/cards/minicard.styl | 11 ++-- client/components/cards/router.js | 15 ------ client/components/lists/body.jade | 2 +- client/components/lists/main.js | 27 +++++----- client/components/lists/main.styl | 12 ++--- client/components/main/popup.js | 2 +- client/components/sidebar/rendered.js | 10 ++-- client/components/sidebar/sidebar.js | 3 +- client/config/router.js | 1 - client/lib/keyboard.js | 7 +-- client/lib/utils.js | 18 ------- client/styles/main.styl | 2 + 22 files changed, 231 insertions(+), 209 deletions(-) delete mode 100644 client/components/cards/router.js diff --git a/.jscsrc b/.jscsrc index cd48d7b7..f3fa8177 100644 --- a/.jscsrc +++ b/.jscsrc @@ -66,11 +66,6 @@ "catch", "typeof" ], - "safeContextKeyword": [ - "self", - "context", - "view" - ], "validateLineBreaks": "LF", "validateQuoteMarks": "'", "validateIndentation": 2, diff --git a/.jshintrc b/.jshintrc index bcb1f698..ebbf3c24 100644 --- a/.jshintrc +++ b/.jshintrc @@ -75,7 +75,6 @@ // XXX Temp, we should remove these "allowIsBoardAdmin": true, "allowIsBoardMember": true, - "BoardSubsManager": true, "currentlyOpenedForm": true, "Emoji": true } diff --git a/client/components/boards/body.jade b/client/components/boards/body.jade index 4abc0baf..4b4c2b90 100644 --- a/client/components/boards/body.jade +++ b/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") diff --git a/client/components/boards/body.js b/client/components/boards/body.js index ffb132c5..5e743001 100644 --- a/client/components/boards/body.js +++ b/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'); diff --git a/client/components/boards/body.styl b/client/components/boards/body.styl index cb351e46..07f35bb8 100644 --- a/client/components/boards/body.styl +++ b/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 diff --git a/client/components/boards/colors.styl b/client/components/boards/colors.styl index 1db44845..a3f74ab8 100644 --- a/client/components/boards/colors.styl +++ b/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%) diff --git a/client/components/boards/router.js b/client/components/boards/router.js index 6845b7f2..9c5bee35 100644 --- a/client/components/boards/router.js +++ b/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); + } +}); diff --git a/client/components/cards/details.jade b/client/components/cards/details.jade index 0de59297..55cc4b9e 100644 --- a/client/components/cards/details.jade +++ b/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 diff --git a/client/components/cards/details.js b/client/components/cards/details.js index cb1a3ef3..d0395129 100644 --- a/client/components/cards/details.js +++ b/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() { diff --git a/client/components/cards/details.styl b/client/components/cards/details.styl index 106a9cfd..68a436f9 100644 --- a/client/components/cards/details.styl +++ b/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 diff --git a/client/components/cards/minicard.styl b/client/components/cards/minicard.styl index a78cd46f..1b9e60b5 100644 --- a/client/components/cards/minicard.styl +++ b/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 diff --git a/client/components/cards/router.js b/client/components/cards/router.js deleted file mode 100644 index 48bb9a95..00000000 --- a/client/components/cards/router.js +++ /dev/null @@ -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); - } -}); diff --git a/client/components/lists/body.jade b/client/components/lists/body.jade index 7bd3392f..dfbe05b7 100644 --- a/client/components/lists/body.jade +++ b/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 diff --git a/client/components/lists/main.js b/client/components/lists/main.js index 78aad17c..8a96f5ce 100644 --- a/client/components/lists/main.js +++ b/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}}); + } } }); }); diff --git a/client/components/lists/main.styl b/client/components/lists/main.styl index ce408990..60a6ab98 100644 --- a/client/components/lists/main.styl +++ b/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 diff --git a/client/components/main/popup.js b/client/components/main/popup.js index dbd99e4d..8abe1697 100644 --- a/client/components/main/popup.js +++ b/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', diff --git a/client/components/sidebar/rendered.js b/client/components/sidebar/rendered.js index 2b58bf33..36b1255c 100644 --- a/client/components/sidebar/rendered.js +++ b/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() { }); }); }); -}; - +}); diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js index 3f0142d4..af676bf2 100644 --- a/client/components/sidebar/sidebar.js +++ b/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() { diff --git a/client/config/router.js b/client/config/router.js index c859013f..2fa1908d 100644 --- a/client/config/router.js +++ b/client/config/router.js @@ -19,7 +19,6 @@ Router.configure({ // Reset default sessions Session.set('error', false); - Session.set('warning', false); Popup.close(); diff --git a/client/lib/keyboard.js b/client/lib/keyboard.js index c1267938..8601e623 100644 --- a/client/lib/keyboard.js +++ b/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() { diff --git a/client/lib/utils.js b/client/lib/utils.js index 9e92e999..7c117d4b 100644 --- a/client/lib/utils.js +++ b/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); }, diff --git a/client/styles/main.styl b/client/styles/main.styl index 0f12e35e..8d1f9591 100644 --- a/client/styles/main.styl +++ b/client/styles/main.styl @@ -229,6 +229,8 @@ dd font-weight: 700 line-height: 18px +.ui-draggable-dragging + z-index: 200 .board-backgrounds-list -- cgit v1.2.3-1-g7c22