From 2c0030da62b9a1e59a55e3429fe514bbd51e1ee3 Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Fri, 29 May 2015 23:35:30 +0200 Subject: Implement multi-selection The UI and the internal APIs are still rough around the edges but the feature is basically working. You can now select multiple cards and move them together or (un|)assign them a label. --- client/components/boards/boardBody.jade | 5 +- client/components/boards/boardBody.js | 19 +- client/components/boards/boardBody.styl | 5 + client/components/boards/boardHeader.jade | 32 ++- client/components/boards/boardHeader.js | 21 +- client/components/boards/colors.styl | 24 +- client/components/boards/router.js | 8 +- client/components/cards/details.styl | 27 --- client/components/cards/labels.styl | 5 + client/components/cards/minicard.jade | 16 +- client/components/cards/minicard.js | 25 ++- client/components/cards/minicard.styl | 119 +++++----- client/components/cards/popups.jade | 5 +- client/components/forms/forms.styl | 68 +++--- client/components/forms/inlinedform.js | 4 +- client/components/lists/body.jade | 9 +- client/components/lists/body.js | 23 +- client/components/lists/main.js | 61 +++-- client/components/lists/main.styl | 5 +- client/components/lists/menu.jade | 1 + client/components/lists/menu.js | 8 + client/components/main/editor.js | 4 +- client/components/main/header.styl | 3 + client/components/main/popup.styl | 239 -------------------- client/components/sidebar/events.js | 17 -- client/components/sidebar/helpers.js | 14 -- client/components/sidebar/sidebar.jade | 39 +--- client/components/sidebar/sidebar.js | 25 ++- client/components/sidebar/sidebar.styl | 30 ++- client/components/sidebar/sidebarFilters.jade | 57 +++++ client/components/sidebar/sidebarFilters.js | 94 ++++++++ client/components/sidebar/templates.html | 77 +++++++ client/components/sidebar/templates.html.old | 307 -------------------------- 33 files changed, 587 insertions(+), 809 deletions(-) create mode 100644 client/components/sidebar/sidebarFilters.jade create mode 100644 client/components/sidebar/sidebarFilters.js create mode 100644 client/components/sidebar/templates.html delete mode 100644 client/components/sidebar/templates.html.old (limited to 'client/components') diff --git a/client/components/boards/boardBody.jade b/client/components/boards/boardBody.jade index 672a3860..57970c4f 100644 --- a/client/components/boards/boardBody.jade +++ b/client/components/boards/boardBody.jade @@ -8,7 +8,10 @@ template(name="board") template(name="boardComponent") if this .board-wrapper(class=colorClass) - .board-canvas(class=sidebarSize) + .board-canvas( + class=sidebarSize + class="{{#if MultiSelection.isActive}}is-multiselection-active{{/if}}" + class="{{#if draggingActive.get}}is-dragging-active{{/if}}") .lists.js-lists each lists +list(this) diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js index cf32f764..b5e4154a 100644 --- a/client/components/boards/boardBody.js +++ b/client/components/boards/boardBody.js @@ -12,14 +12,16 @@ BlazeComponent.extendComponent({ return 'boardComponent'; }, + onCreated: function() { + this.draggingActive = new ReactiveVar(false); + }, + openNewListForm: function() { this.componentChildren('addListForm')[0].open(); }, - showNewCardForms: function(value) { - _.each(this.componentChildren('list'), function(listComponent) { - listComponent.showNewCardForm(value); - }); + setIsDragging: function(bool) { + this.draggingActive.set(bool); }, scrollLeft: function(position) { @@ -79,8 +81,8 @@ BlazeComponent.extendComponent({ helper: 'clone', items: '.js-list:not(.js-list-composer)', placeholder: 'list placeholder', - start: function(event, ui) { - $('.list.placeholder').height(ui.item.height()); + start: function(evt, ui) { + ui.placeholder.height(ui.helper.height()); Popup.close(); }, stop: function() { @@ -97,6 +99,11 @@ BlazeComponent.extendComponent({ } }); + // Disable drag-dropping while in multi-selection mode + self.autorun(function() { + self.$(lists).sortable('option', 'disabled', MultiSelection.isActive()); + }); + // 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) { diff --git a/client/components/boards/boardBody.styl b/client/components/boards/boardBody.styl index de4963ab..70d8f3d6 100644 --- a/client/components/boards/boardBody.styl +++ b/client/components/boards/boardBody.styl @@ -19,6 +19,11 @@ &.next-sidebar margin-right: 248px + &.is-dragging-active + + .open-minicard-composer + display: none + .lists align-items: flex-start display: flex diff --git a/client/components/boards/boardHeader.jade b/client/components/boards/boardHeader.jade index 0ea359fc..f10fcb22 100644 --- a/client/components/boards/boardHeader.jade +++ b/client/components/boards/boardHeader.jade @@ -27,15 +27,43 @@ template(name="headerBoard") i.fa.fa-times-thin else span {{_ 'filter'}} + + if currentUser.isBoardMember + a.board-header-btn.js-multiselection-activate( + title="{{#if MultiSelection.isActive}}{{_ 'filter-on-desc'}}{{/if}}" + class="{{#if MultiSelection.isActive}}emphasis{{/if}}") + i.fa.fa-check-square-o + if MultiSelection.isActive + span Multi-Selection is on + a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}") + i.fa.fa-times-thin + else + span Multi-Selection + .separator a.board-header-btn.js-open-board-menu i.board-header-btn-icon.fa.fa-cog template(name="boardMenuPopup") + if currentUser.isBoardMember + ul.pop-over-list + li: a Archived elements + li: a.js-change-board-color Change color + li: a Permissions + hr ul.pop-over-list - li: a.js-change-board-color Change color li: a Copy this board - li: a Permissions + //- + XXX Language should be handled by sandstorm, but for now display a + language selection link in the board menu. This link is normally present + in the header bar that is not displayed on sandstorm. + if isSandstorm + li: a.js-change-language {{_ 'language'}} + unless isSandstorm + if currentUser.isBoardAdmin + hr + ul.pop-over-list + li: a Close Board… template(name="boardVisibilityList") ul.pop-over-list diff --git a/client/components/boards/boardHeader.js b/client/components/boards/boardHeader.js index a78012ca..28238d4c 100644 --- a/client/components/boards/boardHeader.js +++ b/client/components/boards/boardHeader.js @@ -1,6 +1,7 @@ Template.boardMenuPopup.events({ 'click .js-rename-board': Popup.open('boardChangeTitle'), - 'click .js-change-board-color': Popup.open('boardChangeColor') + 'click .js-change-board-color': Popup.open('boardChangeColor'), + 'click .js-change-language': Popup.open('setLanguage') }); Template.boardChangeTitlePopup.events({ @@ -24,14 +25,15 @@ BlazeComponent.extendComponent({ }, isStarred: function() { - var boardId = this.currentData()._id; + var currentBoard = this.currentData(); var user = Meteor.user(); - return boardId && user && user.hasStarred(boardId); + return currentBoard && user && user.hasStarred(currentBoard._id); }, // Only show the star counter if the number of star is greater than 2 showStarCounter: function() { - return this.currentData().stars > 2; + var currentBoard = this.currentData(); + return currentBoard && currentBoard.stars > 2; }, events: function() { @@ -49,6 +51,17 @@ BlazeComponent.extendComponent({ evt.stopPropagation(); Sidebar.setView(); Filter.reset(); + }, + 'click .js-multiselection-activate': function() { + var currentCard = Session.get('currentCard'); + MultiSelection.activate(); + if (currentCard) { + MultiSelection.add(currentCard); + } + }, + 'click .js-multiselection-reset': function(evt) { + evt.stopPropagation(); + MultiSelection.disable(); } }]; } diff --git a/client/components/boards/colors.styl b/client/components/boards/colors.styl index 2b60dde3..1097b20a 100644 --- a/client/components/boards/colors.styl +++ b/client/components/boards/colors.styl @@ -1,6 +1,10 @@ // We define a set of six board colors that we took from the FlatUI palette. // http://flatuicolors.com - +// +// XXX Centralizing all these properties in a single file just because their +// value is derivedform the same color, doesn't make any sense. We should create +// a macro that would generate 6 version of a given propertie and dispatch this +// list in the other stylus files. setBoardColor(color) &#header, &.sk-spinner div, @@ -8,13 +12,16 @@ setBoardColor(color) .board-list & a background-color: color - & .minicard.is-selected .minicard-details + .is-selected .minicard border-left: 3px solid color - &.pop-over .pop-over-list li a:hover, button[type=submit].primary, input[type=submit].primary background-color: darken(color, 20%) + &.pop-over .pop-over-list li a:hover, + .sidebar-list li a:hover + background-color: lighten(color, 10%) + &#header #header-quick-access ul li.current border-bottom: 2px solid lighten(color, 10%) @@ -28,6 +35,17 @@ setBoardColor(color) &:hover .board-header-btn-close background: darken(complement(color), 20%) + .materialCheckBox.is-checked + border-bottom: 2px solid color + border-right: 2px solid color + + .is-multiselection-active .multi-selection-checkbox + &.is-checked + .minicard + background: lighten(color, 90%) + + &:not(.is-checked) + .minicard:hover:not(.minicard-composer) + background: lighten(color, 97%) + .board-color-nephritis setBoardColor(#27AE60) diff --git a/client/components/boards/router.js b/client/components/boards/router.js index 81fc3d91..e5ccecdb 100644 --- a/client/components/boards/router.js +++ b/client/components/boards/router.js @@ -19,7 +19,6 @@ Router.route('/boards/:_id/:slug', { onAfterAction: function() { // XXX We probably shouldn't rely on Session Session.set('sidebarIsOpen', true); - Session.set('currentWidget', 'home'); Session.set('menuWidgetIsOpen', false); }, waitOn: function() { @@ -37,6 +36,7 @@ Router.route('/boards/:_id/:slug', { Router.route('/boards/:boardId/:slug/:cardId', { name: 'Card', template: 'board', + noEscapeActions: true, onAfterAction: function() { Tracker.nonreactive(function() { if (! Session.get('currentCard') && Sidebar) { @@ -57,7 +57,7 @@ Router.route('/boards/:boardId/:slug/:cardId', { }); // Close the card details pane by pressing escape -EscapeActions.register('detailedPane', - function() { return ! Session.equals('currentCard', null); }, - function() { Utils.goBoardId(Session.get('currentBoard')); } +EscapeActions.register('detailsPane', + function() { Utils.goBoardId(Session.get('currentBoard')); }, + function() { return ! Session.equals('currentCard', null); } ); diff --git a/client/components/cards/details.styl b/client/components/cards/details.styl index 68a436f9..6b1a4cd4 100644 --- a/client/components/cards/details.styl +++ b/client/components/cards/details.styl @@ -134,33 +134,6 @@ .card-composer padding-bottom: 8px -.cc-controls - margin-top: 1px - - input[type="submit"] - float: left - margin-top: 0 - padding: 5px 18px - - .icon-lg - float: left - - .cc-opt - float: right - -.minicard-placeholder, -.minicard.placeholder - background: silver - border: none - min-height: 18px - - .hook - height: 18px - position: absolute - right: 0 - top: 0 - width: 18px - input[type="text"].attachment-add-link-input float: left margin: 0 0 8px diff --git a/client/components/cards/labels.styl b/client/components/cards/labels.styl index 27058b21..9514ce45 100644 --- a/client/components/cards/labels.styl +++ b/client/components/cards/labels.styl @@ -19,6 +19,11 @@ &:hover color: white + &.square + height: 30px + width: @height + padding: 0 + .card-label-green background-color: #3cb500 diff --git a/client/components/cards/minicard.jade b/client/components/cards/minicard.jade index e1176264..ad51ce22 100644 --- a/client/components/cards/minicard.jade +++ b/client/components/cards/minicard.jade @@ -1,7 +1,11 @@ template(name="minicard") - .minicard.card.js-minicard( - class="{{#if isSelected}}is-selected{{/if}}") - a.minicard-details.clearfix.show(href=absoluteUrl) + a.minicard-wrapper.js-minicard(href=absoluteUrl + class="{{#if isSelected}}is-selected{{/if}}" + class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}") + if MultiSelection.isActive + .materialCheckBox.multi-selection-checkbox.js-toggle-multi-selection( + class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}") + .minicard if cover .minicard-cover.js-card-cover(style="background-image: url({{cover.url}});") if labels @@ -16,12 +20,12 @@ template(name="minicard") .badges if comments.count .badge(title="{{_ 'card-comments-title' comments.count }}") - span.badge-icon.icon-sm.fa.fa-comment-o + span.badge-icon.fa.fa-comment-o .badge-text= comments.count if description .badge.badge-state-image-only(title=description) - span.badge-icon.icon-sm.fa.fa-align-left + span.badge-icon.fa.fa-align-left if attachments.count .badge - span.badge-icon.icon-sm.fa.fa-paperclip + span.badge-icon.fa.fa-paperclip span.badge-text= attachments.count diff --git a/client/components/cards/minicard.js b/client/components/cards/minicard.js index b339580b..81d8c0d4 100644 --- a/client/components/cards/minicard.js +++ b/client/components/cards/minicard.js @@ -2,7 +2,6 @@ // 'click .member': Popup.open('cardMember') // }); - BlazeComponent.extendComponent({ template: function() { return 'minicard'; @@ -10,5 +9,29 @@ BlazeComponent.extendComponent({ isSelected: function() { return Session.equals('currentCard', this.currentData()._id); + }, + + toggleMultiSelection: function(evt) { + evt.stopPropagation(); + evt.preventDefault(); + MultiSelection.toogle(this.currentData()._id); + }, + + clickOnMiniCard: function(evt) { + if (MultiSelection.isActive() || evt.shiftKey) { + evt.stopImmediatePropagation(); + evt.preventDefault(); + var methodName = evt.shiftKey ? 'toogleRange' : 'toogle'; + MultiSelection[methodName](this.currentData()._id); + } + }, + + events: function() { + return [{ + submit: this.addCard, + 'click .js-toggle-multi-selection': this.toggleMultiSelection, + 'click .js-minicard': this.clickOnMiniCard, + 'click .open-minicard-composer': this.scrollToBottom + }]; } }).register('minicard'); diff --git a/client/components/cards/minicard.styl b/client/components/cards/minicard.styl index 775d31eb..a5110584 100644 --- a/client/components/cards/minicard.styl +++ b/client/components/cards/minicard.styl @@ -1,30 +1,57 @@ +.minicard-wrapper + cursor: pointer + position: relative + display: flex + align-items: center + margin-bottom: 9px + + &.draggable-hover-card + background-color: #f0f0f0 + border-bottom-color: #c2c2c2 + + &.placeholder + background: darken(white, 20%) + border-radius: 2px + + &.ui-sortable-helper + transform: rotate(4deg) + display: block !important + + .and-n-other + width: 100% + height: 16px + padding: 4px + background-color: darken(white, 5%) + text-align: center + border-radius: 3px + + .multi-selection-checkbox + display: none + + .multi-selection-checkbox + .minicard + margin-left: 8px + .minicard + padding: 6px 8px 2px + position: relative + flex: 1 + flex-wrap: wrap background-color: #fff + min-height: 20px box-shadow: 0 1px 2px rgba(0,0,0,.2) border-radius: 2px - cursor: pointer - margin-bottom: 9px - min-height: 20px - position: relative - z-index: 0 + color: #4d4d4d overflow: hidden transition: transform 0.2s, border-radius 0.2s, border-left 0.2s - a - color: #4d4d4d - - &.active-card - background-color: #f0f0f0 - border-bottom-color: #c2c2c2 - - .minicard-operation - display: block - - &.draggable-hover-card - background-color: #f0f0f0 - border-bottom-color: #c2c2c2 + .is-selected & + transform: translateX(11px) + border-bottom-right-radius: 0 + border-top-right-radius: 0 + z-index: 100 + box-shadow: -2px 1px 2px rgba(0,0,0,.2) .minicard-cover background-position: center @@ -39,21 +66,6 @@ background-size: auto background-position: center - .minicard-details - padding: 6px 8px 2px - position: relative - // z-index: 1 - - &.is-selected - transform: translateX(11px) - border-bottom-right-radius: 0 - border-top-right-radius: 0 - z-index: 100 - box-shadow: -2px 1px 2px rgba(0,0,0,.2) - - a.minicard-details - text-decoration:none - .minicard-details-overlay background: transparent bottom: 0 @@ -121,23 +133,24 @@ .minicard-members:empty display: none - &.ui-sortable-helper - transform: rotate(4deg) - -.badges - float: left - - &:empty - display: none - -textarea.minicard-composer-textarea, -textarea.minicard-composer-textarea:focus - background: none - border: none - box-shadow: none - height: auto - margin-bottom: 4px - padding: 0 - max-height: 162px - min-height: 54px - overflow-y: auto + .badges + float: left + + &:empty + display: none + + &.minicard-composer + margin-bottom: 10px + + textarea.minicard-composer-textarea, + textarea.minicard-composer-textarea:focus + resize: none + background: none + border: none + box-shadow: none + height: auto + margin: 0 + padding: 0 + max-height: 162px + min-height: 54px + overflow-y: auto diff --git a/client/components/cards/popups.jade b/client/components/cards/popups.jade index 0b5aa4c0..0d10c147 100644 --- a/client/components/cards/popups.jade +++ b/client/components/cards/popups.jade @@ -1,8 +1,7 @@ template(name="cardMembersPopup") - //- input.js-search-mem(autofocus placeholder="Search members…" type="text") - ul.pop-over-member-list.checkable.js-mem-list + ul.pop-over-member-list.js-mem-list each board.members - li.item.js-member-item(class="{{#if isCardMember}}active{{/if}}") + li.item(class="{{#if isCardMember}}active{{/if}}") a.name.js-select-member(href="#") +userAvatar(user=user size="small") span.full-name diff --git a/client/components/forms/forms.styl b/client/components/forms/forms.styl index c572863d..06796170 100644 --- a/client/components/forms/forms.styl +++ b/client/components/forms/forms.styl @@ -30,10 +30,6 @@ input[type="radio"] -webkit-appearance: radio min-height: inherit -input[type="checkbox"] - -webkit-appearance: checkbox - margin-right: 4px - input[type="text"], input[type="password"], input[type="email"] @@ -182,10 +178,6 @@ fieldset input[type="hidden"] display: none -input[type="checkbox"], -input[type="radio"] - display: inline - .radio-div, .check-div display: block @@ -233,6 +225,36 @@ textarea font-size: 26px margin: 3px 4px +// Material Design checkboxes +[type="checkbox"]:not(:checked), +[type="checkbox"]:checked + position: absolute + left: -9999px + visibility: hidden + +.materialCheckBox + position: relative + width: 13px + height: @width + z-index: 0 + border: 2px solid #5a5a5a + border-radius: 1px + transition: .2s + margin: 0 + cursor: pointer + + &.is-checked + top: -4px + left: -3px + width: 7px + height: 15px + margin-right: 6px + border-top: 2px solid transparent + border-left: 2px solid transparent + transform: rotate(40deg) + -webkit-backface-visibility: hidden + transform-origin: 100% 100% + .button-link background: #fff background: linear-gradient(#fff, #f5f5f5) @@ -355,9 +377,6 @@ textarea background-color: rgba(255, 255, 255, .3) border-color: transparent - .icon-sm - color: #fff - &:active background: #2e85b8 background: linear-gradient(#2e85b8, #28739f) @@ -401,7 +420,6 @@ textarea border-color: #8b0e0e button - &.quiet-button, &.loud-text-button background: none @@ -438,11 +456,6 @@ button &.w-img padding-left: 28px - .icon-sm - left: 6px - position: absolute - top: 6px - &:hover color: #4d4d4d background: #dcdcdc @@ -575,29 +588,8 @@ button border-color: #2e85b8 color: #fff -.form-grid - display: flex - flex-wrap: wrap - width: 100% - -.form-grid-child - flex: 1 - margin: 0 0 8px - -.form-grid-child-full - flex: 1 1 100% - -.form-grid-child-threequarters - flex: 3 - margin-right: 8px - -.form-grid-child-twothirds - flex: 2 - margin-right: 8px - .dropdown-menu border-radius: 2px - // padding-bottom: 3px overflow: hidden li diff --git a/client/components/forms/inlinedform.js b/client/components/forms/inlinedform.js index f2774084..b8442a28 100644 --- a/client/components/forms/inlinedform.js +++ b/client/components/forms/inlinedform.js @@ -97,6 +97,6 @@ BlazeComponent.extendComponent({ // Press escape to close the currently opened inlinedForm EscapeActions.register('inlinedForm', - function() { return currentlyOpenedForm.get() !== null; }, - function() { currentlyOpenedForm.get().close(); } + function() { currentlyOpenedForm.get().close(); }, + function() { return currentlyOpenedForm.get() !== null; } ); diff --git a/client/components/lists/body.jade b/client/components/lists/body.jade index 3e769206..3e780850 100644 --- a/client/components/lists/body.jade +++ b/client/components/lists/body.jade @@ -10,13 +10,12 @@ template(name="listBody") +inlinedForm(autoclose=false position="bottom") +addCardForm(listId=_id position="bottom") else - if newCardFormIsVisible.get - a.open-card-composer.js-open-inlined-form - i.fa.fa-plus - | {{_ 'add-card'}} + a.open-minicard-composer.js-open-inlined-form + i.fa.fa-plus + | {{_ 'add-card'}} template(name="addCardForm") - .minicard.js-composer + .minicard.minicard-composer.js-composer .minicard-labels.js-minicard-composer-labels .minicard-details.clearfix textarea.minicard-composer-textarea.js-card-title(autofocus) diff --git a/client/components/lists/body.js b/client/components/lists/body.js index 8400af96..04f122cb 100644 --- a/client/components/lists/body.js +++ b/client/components/lists/body.js @@ -34,18 +34,17 @@ BlazeComponent.extendComponent({ } if ($.trim(title)) { - Cards.insert({ + var _id = Cards.insert({ title: title, listId: this.data()._id, boardId: this.data().board()._id, sort: sortIndex - }, function(err, _id) { - // In case the filter is active we need to add the newly inserted card - // in the list of exceptions -- cards that are not filtered. Otherwise - // the card will disappear instantly. - // See https://github.com/libreboard/libreboard/issues/80 - Filter.addException(_id); }); + // In case the filter is active we need to add the newly inserted card in + // the list of exceptions -- cards that are not filtered. Otherwise the + // card will disappear instantly. + // See https://github.com/libreboard/libreboard/issues/80 + Filter.addException(_id); // We keep the form opened, empty it, and scroll to it. textarea.val('').focus(); @@ -55,10 +54,6 @@ BlazeComponent.extendComponent({ } }, - showNewCardForm: function(value) { - this.newCardFormIsVisible.set(value); - }, - scrollToBottom: function() { var container = this.firstNode(); $(container).animate({ @@ -66,14 +61,10 @@ BlazeComponent.extendComponent({ }); }, - onCreated: function() { - this.newCardFormIsVisible = new ReactiveVar(true); - }, - events: function() { return [{ submit: this.addCard, - 'click .open-card-composer': this.scrollToBottom + 'click .open-minicard-composer': this.scrollToBottom }]; } }).register('listBody'); diff --git a/client/components/lists/main.js b/client/components/lists/main.js index beae784d..bcdba7c4 100644 --- a/client/components/lists/main.js +++ b/client/components/lists/main.js @@ -8,10 +8,6 @@ BlazeComponent.extendComponent({ this.componentChildren('listBody')[0].openForm(options); }, - showNewCardForm: function(value) { - this.componentChildren('listBody')[0].showNewCardForm(value); - }, - onCreated: function() { this.newCardFormIsVisible = new ReactiveVar(true); }, @@ -35,30 +31,59 @@ BlazeComponent.extendComponent({ connectWith: '.js-minicards', tolerance: 'pointer', appendTo: '.js-lists', - helper: 'clone', + helper: function(evt, item) { + var helper = item.clone(); + if (MultiSelection.isActive()) { + var andNOthers = $cards.find('.js-minicard.is-checked').length - 1; + if (andNOthers > 0) { + helper.append($(Blaze.toHTML(HTML.DIV( + // XXX Super bad class name + {'class': 'and-n-other'}, + // XXX Need to translate + 'and ' + andNOthers + ' other cards.' + )))); + } + } + return helper; + }, items: itemsSelector, - placeholder: 'minicard placeholder', - start: function(event, ui) { + placeholder: 'minicard-wrapper placeholder', + start: function(evt, ui) { ui.placeholder.height(ui.helper.height()); - Popup.close(); - boardComponent.showNewCardForms(false); + EscapeActions.executeLowerThan('popup'); + boardComponent.setIsDragging(true); }, - stop: function(event, ui) { + stop: function(evt, ui) { // To attribute the new index number, we need to get the dom element // of the previous and the following card -- if any. var cardDomElement = ui.item.get(0); var prevCardDomElement = ui.item.prev('.js-minicard').get(0); var nextCardDomElement = ui.item.next('.js-minicard').get(0); var sort = Utils.getSortIndex(prevCardDomElement, nextCardDomElement); - var cardId = Blaze.getData(cardDomElement)._id; var listId = Blaze.getData(ui.item.parents('.list').get(0))._id; - Cards.update(cardId, { - $set: { - listId: listId, - sort: sort - } - }); - boardComponent.showNewCardForms(true); + + if (MultiSelection.isActive()) { + Cards.find(MultiSelection.getMongoSelector()).forEach(function(c) { + Cards.update(c._id, { + $set: { + listId: listId, + sort: sort + } + }); + }); + } else { + var cardId = Blaze.getData(cardDomElement)._id; + Cards.update(cardId, { + $set: { + listId: listId, + // XXX Using the same sort index for multiple cards is + // unacceptable. Keep that only until we figure out if we want to + // refactor the whole sorting mecanism or do something more basic. + sort: sort + } + }); + } + boardComponent.setIsDragging(false); } }); diff --git a/client/components/lists/main.styl b/client/components/lists/main.styl index 47dfcf28..3e51ac08 100644 --- a/client/components/lists/main.styl +++ b/client/components/lists/main.styl @@ -93,10 +93,13 @@ overflow-y: auto padding: 5px 11px + .minicards form + margin-bottom: 9px + .ps-scrollbar-y-rail transform: translateX(2px) -.open-card-composer +.open-minicard-composer border-radius: 2px color: #8c8c8c display: block diff --git a/client/components/lists/menu.jade b/client/components/lists/menu.jade index ff7820a4..052f064c 100644 --- a/client/components/lists/menu.jade +++ b/client/components/lists/menu.jade @@ -5,6 +5,7 @@ template(name="listActionPopup") if cards.count hr ul.pop-over-list + li: a.js-select-cards {{_ 'list-select-cards'}} li: a.js-move-cards {{_ 'list-move-cards'}} li: a.js-archive-cards {{_ 'list-archive-cards'}} hr diff --git a/client/components/lists/menu.js b/client/components/lists/menu.js index f2abd3bf..dda1270c 100644 --- a/client/components/lists/menu.js +++ b/client/components/lists/menu.js @@ -6,6 +6,14 @@ Template.listActionPopup.events({ Popup.close(); }, 'click .js-list-subscribe': function() {}, + 'click .js-select-cards': function() { + var cardIds = Cards.find( + {listId: this._id}, + {fields: { _id: 1 }} + ).map(function(card) { return card._id; }); + MultiSelection.add(cardIds); + Popup.close(); + }, 'click .js-move-cards': Popup.open('listMoveCards'), 'click .js-archive-cards': Popup.afterConfirm('listArchiveCards', function() { Cards.find({listId: this._id}).forEach(function(card) { diff --git a/client/components/main/editor.js b/client/components/main/editor.js index a35ecd06..e1a90cb1 100644 --- a/client/components/main/editor.js +++ b/client/components/main/editor.js @@ -61,6 +61,6 @@ Template.editor.onRendered(function() { }); EscapeActions.register('textcomplete', - function() { return dropdownMenuIsOpened; }, - function() {} + function() {}, + function() { return dropdownMenuIsOpened; } ); diff --git a/client/components/main/header.styl b/client/components/main/header.styl index 248e2851..8e1682eb 100644 --- a/client/components/main/header.styl +++ b/client/components/main/header.styl @@ -58,6 +58,9 @@ margin: 4px 8px 0 0 float: left + i.fa-chevron-down + margin-right: 4px + #header-main-bar height: 28px * 1.618034 - 6px padding: 7px 10px 0 diff --git a/client/components/main/popup.styl b/client/components/main/popup.styl index 141f4261..cf1fd46e 100644 --- a/client/components/main/popup.styl +++ b/client/components/main/popup.styl @@ -35,21 +35,9 @@ margin: 4px 0 12px width: 100% - .empty - margin: 0 - img max-width: 270px - .custom-image img - height: 18px - left: 9px - top: 9px - width: 18px - - .title - line-height: 32px - .header height: 36px position: relative @@ -68,10 +56,6 @@ text-overflow: ellipsis white-space: nowrap - .back-btn, .close-btn - &:hover .icon-sm - color: darken(white, 80%) - .back-btn float: left overflow: hidden @@ -91,7 +75,6 @@ top: 0 right: 0 - &.no-title .header background: none @@ -134,15 +117,11 @@ margin-bottom: 8px .pop-over-list - &.navigable li.not-selectable>a:hover, li.not-selectable>a:hover color: #8c8c8c cursor: default - .icon-sm - color: #a6a6a6 - li > a cursor: pointer display: block @@ -168,9 +147,6 @@ .unread-indicator background: #fff - .icon-sm - color: #fff - .sub-name clear: both color: #8c8c8c @@ -208,9 +184,6 @@ .vis-icon opacity: .35 - .icon-sm - color: #a6a6a6 - &:hover background: none @@ -218,9 +191,6 @@ .quiet color: #8c8c8c - .icon-sm - color: #a6a6a6 - &:active background: none @@ -268,9 +238,6 @@ .quiet color: #8c8c8c - .icon-sm - color: #a6a6a6 - li.selected > a background-color: #005377 color: #fff @@ -287,14 +254,10 @@ .unread-indicator background: #fff - .icon-sm - color: #fff - &:active background-color: #005377 .pop-over.miniprofile - .header border-bottom-color: transparent height: 30px @@ -329,205 +292,3 @@ &:hover text-decoration: underline - -.pop-over.avdetail .header - border-bottom-color: transparent - height: 20px - position: absolute - top: 8px - left: 8px - right: 8px - z-index: 0 - -.pop-over.avdetail .header-title - display: none - -.pop-over.avdetail .content - text-align: center - -.pop-over.avdetail .mem-info - margin: 2px 24px 8px - position: relative - z-index: 1 - width: 222px - -.pop-over.avdetail .mem-info h3 a - text-decoration: none - -.pop-over.avdetail .mem-info h3 a:hover - text-decoration: underline - -.pop-over-label-list li, -.pop-over-member-list li - - &.disabled a - cursor:default - - &:not(.disabled):hover a - background-color: #005377 - color: #fff - - -.pop-over-label-list, -.pop-over-member-list, -.pop-over-emoji-list, -.pop-over-card-list - li - a - border-radius: 3px - display: block - height: 30px - line-height: 30px - overflow: hidden - position: relative - text-overflow: ellipsis - text-decoration: none - white-space: nowrap - padding: 4px - margin-bottom: 2px - - &.multi-line - line-height: 16px - - .member - margin-right: 8px - - .card-label - float: left - height: 30px - margin: 0 8px 0 0 - padding: 0 - width: 30px - - .option, - .icon-check - background-clip: content-box - background-origin: content-box - padding: 11px - position: absolute - top: 0 - right: 0 - - .sub-name - font-size: 12px - - - &:last-child a - margin-bottom: 0 - - &.disabled - opacity: .5 - - &.active a, - &.selected a - background: none - color: #4d4d4d - cursor: default - - .quiet - color: #8c8c8c - - &.email-invite - - .member - display: none - - a - padding: 0 10px - - &.selected a - background-color: #005377 - color: #fff - - .quiet - color: #eee - - .card-label - border-radius: 3px - - .icon-check - color: #fff - - &.active a .icon-check - display: block - - &.unconfirmed a.name - line-height: 16px - - &.options li - - &.selected a - padding-right: 28px - - .option - display: block - opacity: .5 - - &:hover - opacity: 1 - - &.disabled.selected a - padding-right: 0 - - .option - display: none - - - &.no-option.selected a - padding-right: 6px - - .option - display: none - - &.collapsed - - &.checkable li.active a - padding-right: 0 - - li - float: left - margin: 0 3px 3px 0 - - a - padding: 0 - margin: 0 - width: 30px - - .member - opacity: .8 - - .full-name - display: none - - &.selected a .member, - &.active.selected a .member - border-color: #005377 - opacity: .9 - - &.active a - - .member - border-color: #2e85b8 - opacity: 1 - - .icon-check - border-radius: 3px - background-color: #2e85b8 - bottom: 0 - color: #fff - display: block - padding: 0 - right: 0 - top: auto - - &.checkable li.active a - padding-right: 28px - - &.filtered li - display: none - - &.matches-filter - display: block - - &.limited li.exceeds-limit - display: none diff --git a/client/components/sidebar/events.js b/client/components/sidebar/events.js index 1067421f..a1aeb13a 100644 --- a/client/components/sidebar/events.js +++ b/client/components/sidebar/events.js @@ -1,20 +1,3 @@ -Template.filterSidebar.events({ - 'click .js-toggle-label-filter': function(event) { - Filter.labelIds.toogle(this._id); - Filter.resetExceptions(); - event.preventDefault(); - }, - 'click .js-toogle-member-filter': function(event) { - Filter.members.toogle(this._id); - Filter.resetExceptions(); - event.preventDefault(); - }, - 'click .js-clear-all': function(event) { - Filter.reset(); - event.preventDefault(); - } -}); - var getMemberIndex = function(board, searchId) { for (var i = 0; i < board.members.length; i++) { if (board.members[i].userId === searchId) diff --git a/client/components/sidebar/helpers.js b/client/components/sidebar/helpers.js index 15035bd4..9d3340ad 100644 --- a/client/components/sidebar/helpers.js +++ b/client/components/sidebar/helpers.js @@ -1,17 +1,3 @@ -var widgetTitles = { - filter: 'filter-cards', - background: 'change-background' -}; - -Template.sidebar.helpers({ - currentWidget: function() { - return Session.get('currentWidget') + 'Sidebar'; - }, - currentWidgetTitle: function() { - return TAPi18n.__(widgetTitles[Session.get('currentWidget')]); - } -}); - // Template.addMemberPopup.helpers({ // isBoardMember: function() { // var user = Users.findOne(this._id); diff --git a/client/components/sidebar/sidebar.jade b/client/components/sidebar/sidebar.jade index 07d6bbcf..9dd47b0d 100644 --- a/client/components/sidebar/sidebar.jade +++ b/client/components/sidebar/sidebar.jade @@ -4,49 +4,22 @@ template(name="sidebar") class="{{#if isTongueHidden}}is-hidden{{/if}}") i.fa.fa-chevron-left .sidebar-content.js-board-sidebar-content.js-perfect-scrollbar + unless isDefaultView + h2 + a.fa.fa-chevron-left.js-back-home + = getViewTitle +Template.dynamic(template=getViewTemplate) template(name='homeSidebar') +membersWidget - hr.clear + hr +labelsWidget - hr.clear + hr h3 i.fa.fa-comments-o | {{_ 'activities'}} +activities(mode="board") -template(name="filterSidebar") - ul.pop-over-label-list.checkable - each currentBoard.labels - li.item.matches-filter - a.name.js-toggle-label-filter - span.card-label(class="card-label-{{color}}") - span.full-name - if name - = name - else - span.quiet {{_ "label-default" color}} - if Filter.labelIds.isSelected _id}} - span.icon-sm.fa.fa-check - hr - ul.pop-over-member-list.checkable - each currentBoard.members - if isActive - with getUser userId - li.item.js-member-item( - class="{{#if Filter.members.isSelected _id}}active{{/if}}") - a.name.js-toogle-member-filter - +userAvatar(user=this size="small") - span.full-name - = profile.name - | ({{ username }}) - if Filter.members.isSelected _id - span.icon-sm.fa.fa-check - hr - a.js-clear-all(class="{{#unless Filter.isActive}}disabled{{/unless}}") - | {{_ 'filter-clear'}} - template(name="membersWidget") .board-widget.board-widget-members h3 diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js index b737e9de..777d72e1 100644 --- a/client/components/sidebar/sidebar.js +++ b/client/components/sidebar/sidebar.js @@ -1,6 +1,11 @@ +Sidebar = null; + var defaultView = 'home'; -Sidebar = null; +var viewTitles = { + filter: 'filter-cards', + multiselection: 'multi-selection' +}; BlazeComponent.extendComponent({ template: function() { @@ -60,14 +65,23 @@ BlazeComponent.extendComponent({ }, setView: function(view) { - view = view || defaultView; + view = _.isString(view) ? view : defaultView; this._view.set(view); + this.open(); + }, + + isDefaultView: function() { + return this.getView() === defaultView; }, getViewTemplate: function() { return this.getView() + 'Sidebar'; }, + getViewTitle: function() { + return TAPi18n.__(viewTitles[this.getView()]); + }, + // Board members can assign people or labels by drag-dropping elements from // the sidebar to the cards on the board. In order to re-initialize the // jquery-ui plugin any time a draggable member or label is modified or @@ -108,12 +122,13 @@ BlazeComponent.extendComponent({ // XXX Hacky, we need some kind of `super` var mixinEvents = this.getMixin(Mixins.InfiniteScrolling).events(); return mixinEvents.concat([{ - 'click .js-toogle-sidebar': this.toogle + 'click .js-toogle-sidebar': this.toogle, + 'click .js-back-home': this.setView }]); } }).register('sidebar'); EscapeActions.register('sidebarView', - function() { return Sidebar && Sidebar.getView() !== defaultView; }, - function() { Sidebar.setView(defaultView); } + function() { Sidebar.setView(defaultView); }, + function() { return Sidebar && Sidebar.getView() !== defaultView; } ); diff --git a/client/components/sidebar/sidebar.styl b/client/components/sidebar/sidebar.styl index a5bc3dc5..e24b7de2 100644 --- a/client/components/sidebar/sidebar.styl +++ b/client/components/sidebar/sidebar.styl @@ -7,7 +7,7 @@ right: 0 .sidebar-content - padding: 10px 20px + padding: 12px background: white box-shadow: -10px 0px 5px -10px darken(white, 30%) z-index: 10 @@ -23,7 +23,33 @@ color: darken(white, 50%) hr - margin: 8px 0 + margin: 13px 0 + + ul.sidebar-list + display: flex + flex-direction: column + + li a + display: flex + height: 30px + margin: 0 + padding: 4px + border-radius: 3px + align-items: center + + &:hover + &, i, .quiet + color white + + .member, .card-label + margin-right: 7px + + .sidebar-list-item-description + flex: 1 + overflow: ellipsis + + .fa.fa-check + margin: 0 4px .board-sidebar width: 248px diff --git a/client/components/sidebar/sidebarFilters.jade b/client/components/sidebar/sidebarFilters.jade new file mode 100644 index 00000000..34b3074f --- /dev/null +++ b/client/components/sidebar/sidebarFilters.jade @@ -0,0 +1,57 @@ +//- + XXX There is a *lot* of code duplication in the above templates and in the + corresponding JavaScript components. We will probably need the upcoming #let + and #each x in y constructors. + +template(name="filterSidebar") + ul.sidebar-list + each currentBoard.labels + li + a.name.js-toggle-label-filter + span.card-label.square(class="card-label-{{color}}") + span.sidebar-list-item-description + if name + = name + else + span.quiet {{_ "label-default" color}} + if Filter.labelIds.isSelected _id + i.fa.fa-check + hr + ul.sidebar-list + each currentBoard.members + if isActive + with getUser userId + li(class="{{#if Filter.members.isSelected _id}}active{{/if}}") + a.name.js-toogle-member-filter + +userAvatar(user=this size="small") + span.sidebar-list-item-description + = profile.name + | ({{ username }}) + if Filter.members.isSelected _id + i.fa.fa-check + hr + a.js-clear-all(class="{{#unless Filter.isActive}}disabled{{/unless}}") + | {{_ 'filter-clear'}} + +template(name="multiselectionSidebar") + ul.sidebar-list + each currentBoard.labels + li + a.name.js-toggle-label-multiselection + span.card-label.square(class="card-label-{{color}}") + span.sidebar-list-item-description + if name + = name + else + span.quiet {{_ "label-default" color}} + if allSelectedElementHave 'label' _id + i.fa.fa-check + else if someSelectedElementHave 'label' _id + i.fa.fa-ellipsis-h + //- + XXX We should be able to assign a member to the list of selected cards. + +template(name="disambiguateMultiLabelPopup") + p What do you want to do? + button.wide.js-remove-label Remove the label + button.wide.js-add-label Add the label diff --git a/client/components/sidebar/sidebarFilters.js b/client/components/sidebar/sidebarFilters.js new file mode 100644 index 00000000..df0db529 --- /dev/null +++ b/client/components/sidebar/sidebarFilters.js @@ -0,0 +1,94 @@ +BlazeComponent.extendComponent({ + template: function() { + return 'filterSidebar'; + }, + + events: function() { + return [{ + 'click .js-toggle-label-filter': function(event) { + Filter.labelIds.toogle(this._id); + Filter.resetExceptions(); + event.preventDefault(); + }, + 'click .js-toogle-member-filter': function(event) { + Filter.members.toogle(this._id); + Filter.resetExceptions(); + event.preventDefault(); + }, + 'click .js-clear-all': function(event) { + Filter.reset(); + event.preventDefault(); + } + }]; + } +}).register('filterSidebar'); + +var updateSelectedCards = function(query) { + Cards.find(MultiSelection.getMongoSelector()).forEach(function(card) { + Cards.update(card._id, query); + }); +}; + +BlazeComponent.extendComponent({ + template: function() { + return 'multiselectionSidebar'; + }, + + mapSelection: function(kind, _id) { + return Cards.find(MultiSelection.getMongoSelector()).map(function(card) { + var methodName = kind === 'label' ? 'hasLabel' : 'isAssigned'; + return card[methodName](_id); + }); + }, + + allSelectedElementHave: function(kind, _id) { + if (MultiSelection.isEmpty()) + return false; + else + return _.every(this.mapSelection(kind, _id)); + }, + + someSelectedElementHave: function(kind, _id) { + if (MultiSelection.isEmpty()) + return false; + else + return _.some(this.mapSelection(kind, _id)); + }, + + events: function() { + return [{ + 'click .js-toggle-label-multiselection': function(evt, tpl) { + var labelId = this.currentData()._id; + var mappedSelection = this.mapSelection('label', labelId); + var operation; + if (_.every(mappedSelection)) + operation = '$pull'; + else if (_.every(mappedSelection, function(bool) { return ! bool; })) + operation = '$addToSet'; + else { + var popup = Popup.open('disambiguateMultiLabel'); + // XXX We need to have a better integration between the popup and the + // UI components systems. + return popup.call(this.currentData(), evt, tpl); + } + + var query = {}; + query[operation] = { + labelIds: labelId + }; + updateSelectedCards(query); + } + }]; + } +}).register('multiselectionSidebar'); + +Template.disambiguateMultiLabelPopup.events({ + 'click .js-remove-label': function() { + updateSelectedCards({$pull: {labelIds: this._id}}); + Popup.close(); + }, + 'click .js-add-label': function() { + updateSelectedCards({$addToSet: {labelIds: this._id}}); + Popup.close(); + } +}); diff --git a/client/components/sidebar/templates.html b/client/components/sidebar/templates.html new file mode 100644 index 00000000..12e7be0a --- /dev/null +++ b/client/components/sidebar/templates.html @@ -0,0 +1,77 @@ + + + + + + + + diff --git a/client/components/sidebar/templates.html.old b/client/components/sidebar/templates.html.old deleted file mode 100644 index d8b063f0..00000000 --- a/client/components/sidebar/templates.html.old +++ /dev/null @@ -1,307 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - -- cgit v1.2.3-1-g7c22