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.
This commit is contained in:
Maxime Quandalle 2015-05-29 23:35:30 +02:00
parent 6457615e6a
commit 2c0030da62
45 changed files with 883 additions and 933 deletions

1
.gitignore vendored
View file

@ -3,3 +3,4 @@
.meteor-spk .meteor-spk
.tx/ .tx/
*.sublime-workspace *.sublime-workspace
tmp/

142
.jscsrc
View file

@ -1,73 +1,73 @@
{ {
"disallowSpacesInNamedFunctionExpression": { "disallowSpacesInNamedFunctionExpression": {
"beforeOpeningRoundBrace": true "beforeOpeningRoundBrace": true
}, },
"disallowSpacesInFunctionExpression": { "disallowSpacesInFunctionExpression": {
"beforeOpeningRoundBrace": true "beforeOpeningRoundBrace": true
}, },
"disallowSpacesInAnonymousFunctionExpression": { "disallowSpacesInAnonymousFunctionExpression": {
"beforeOpeningRoundBrace": true "beforeOpeningRoundBrace": true
}, },
"disallowSpacesInFunctionDeclaration": { "disallowSpacesInFunctionDeclaration": {
"beforeOpeningRoundBrace": true "beforeOpeningRoundBrace": true
}, },
"disallowEmptyBlocks": true, "disallowEmptyBlocks": true,
"disallowSpacesInsideArrayBrackets": true, "disallowSpacesInsideArrayBrackets": true,
"disallowSpacesInsideParentheses": true, "disallowSpacesInsideParentheses": true,
"disallowQuotedKeysInObjects": "allButReserved", "disallowQuotedKeysInObjects": "allButReserved",
"disallowSpaceAfterObjectKeys": true, "disallowSpaceAfterObjectKeys": true,
"disallowSpaceAfterPrefixUnaryOperators": [ "disallowSpaceAfterPrefixUnaryOperators": [
"++", "++",
"--", "--",
"+", "+",
"-", "-",
"~" "~"
], ],
"disallowSpaceBeforePostfixUnaryOperators": true, "disallowSpaceBeforePostfixUnaryOperators": true,
"disallowSpaceBeforeBinaryOperators": [ "disallowSpaceBeforeBinaryOperators": [
"," ","
], ],
"disallowMixedSpacesAndTabs": true, "disallowMixedSpacesAndTabs": true,
"disallowTrailingWhitespace": true, "disallowTrailingWhitespace": true,
"disallowTrailingComma": true, "disallowTrailingComma": true,
"disallowYodaConditions": true, "disallowYodaConditions": true,
"disallowKeywords": [ "with" ], "disallowKeywords": [ "with" ],
"disallowMultipleLineBreaks": true, "disallowMultipleLineBreaks": true,
"disallowMultipleVarDecl": "exceptUndefined", "disallowMultipleVarDecl": "exceptUndefined",
"requireSpaceBeforeBlockStatements": true, "requireSpaceBeforeBlockStatements": true,
"requireParenthesesAroundIIFE": true, "requireParenthesesAroundIIFE": true,
"requireSpacesInConditionalExpression": true, "requireSpacesInConditionalExpression": true,
"requireBlocksOnNewline": 1, "requireBlocksOnNewline": 1,
"requireCommaBeforeLineBreak": true, "requireCommaBeforeLineBreak": true,
"requireSpaceAfterPrefixUnaryOperators": [ "requireSpaceAfterPrefixUnaryOperators": [
"!" "!"
], ],
"requireSpaceBeforeBinaryOperators": true, "requireSpaceBeforeBinaryOperators": true,
"requireSpaceAfterBinaryOperators": true, "requireSpaceAfterBinaryOperators": true,
"requireCamelCaseOrUpperCaseIdentifiers": true, "requireCamelCaseOrUpperCaseIdentifiers": true,
"requireLineFeedAtFileEnd": true, "requireLineFeedAtFileEnd": true,
"requireCapitalizedConstructors": true, "requireCapitalizedConstructors": true,
"requireDotNotation": true, "requireDotNotation": true,
"requireSpacesInForStatement": true, "requireSpacesInForStatement": true,
"requireSpaceBetweenArguments": true, "requireSpaceBetweenArguments": true,
"requireCurlyBraces": [ "requireCurlyBraces": [
"do" "do"
], ],
"requireSpaceAfterKeywords": [ "requireSpaceAfterKeywords": [
"if", "if",
"else", "else",
"for", "for",
"while", "while",
"do", "do",
"switch", "switch",
"case", "case",
"return", "return",
"try", "try",
"catch", "catch",
"typeof" "typeof"
], ],
"validateLineBreaks": "LF", "validateLineBreaks": "LF",
"validateQuoteMarks": "'", "validateQuoteMarks": "'",
"validateIndentation": 2, "validateIndentation": 2,
"maximumLineLength": 80 "maximumLineLength": 80
} }

View file

@ -69,9 +69,10 @@
// Our objects // Our objects
"EscapeActions": true, "EscapeActions": true,
"Filter": true, "Filter": true,
"Mixins": true,
"Popup": true,
"Filter": true, "Filter": true,
"Mixins": true,
"MultiSelection": true,
"Popup": true,
"Sidebar": true, "Sidebar": true,
"Utils": true, "Utils": true,

View file

@ -8,7 +8,10 @@ template(name="board")
template(name="boardComponent") template(name="boardComponent")
if this if this
.board-wrapper(class=colorClass) .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 .lists.js-lists
each lists each lists
+list(this) +list(this)

View file

@ -12,14 +12,16 @@ BlazeComponent.extendComponent({
return 'boardComponent'; return 'boardComponent';
}, },
onCreated: function() {
this.draggingActive = new ReactiveVar(false);
},
openNewListForm: function() { openNewListForm: function() {
this.componentChildren('addListForm')[0].open(); this.componentChildren('addListForm')[0].open();
}, },
showNewCardForms: function(value) { setIsDragging: function(bool) {
_.each(this.componentChildren('list'), function(listComponent) { this.draggingActive.set(bool);
listComponent.showNewCardForm(value);
});
}, },
scrollLeft: function(position) { scrollLeft: function(position) {
@ -79,8 +81,8 @@ BlazeComponent.extendComponent({
helper: 'clone', helper: 'clone',
items: '.js-list:not(.js-list-composer)', items: '.js-list:not(.js-list-composer)',
placeholder: 'list placeholder', placeholder: 'list placeholder',
start: function(event, ui) { start: function(evt, ui) {
$('.list.placeholder').height(ui.item.height()); ui.placeholder.height(ui.helper.height());
Popup.close(); Popup.close();
}, },
stop: function() { 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 // If there is no data in the board (ie, no lists) we autofocus the list
// creation form by clicking on the corresponding element. // creation form by clicking on the corresponding element.
if (self.data().lists().count() === 0) { if (self.data().lists().count() === 0) {

View file

@ -19,6 +19,11 @@
&.next-sidebar &.next-sidebar
margin-right: 248px margin-right: 248px
&.is-dragging-active
.open-minicard-composer
display: none
.lists .lists
align-items: flex-start align-items: flex-start
display: flex display: flex

View file

@ -27,15 +27,43 @@ template(name="headerBoard")
i.fa.fa-times-thin i.fa.fa-times-thin
else else
span {{_ 'filter'}} 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 .separator
a.board-header-btn.js-open-board-menu a.board-header-btn.js-open-board-menu
i.board-header-btn-icon.fa.fa-cog i.board-header-btn-icon.fa.fa-cog
template(name="boardMenuPopup") 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 ul.pop-over-list
li: a.js-change-board-color Change color
li: a Copy this board 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") template(name="boardVisibilityList")
ul.pop-over-list ul.pop-over-list

View file

@ -1,6 +1,7 @@
Template.boardMenuPopup.events({ Template.boardMenuPopup.events({
'click .js-rename-board': Popup.open('boardChangeTitle'), '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({ Template.boardChangeTitlePopup.events({
@ -24,14 +25,15 @@ BlazeComponent.extendComponent({
}, },
isStarred: function() { isStarred: function() {
var boardId = this.currentData()._id; var currentBoard = this.currentData();
var user = Meteor.user(); 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 // Only show the star counter if the number of star is greater than 2
showStarCounter: function() { showStarCounter: function() {
return this.currentData().stars > 2; var currentBoard = this.currentData();
return currentBoard && currentBoard.stars > 2;
}, },
events: function() { events: function() {
@ -49,6 +51,17 @@ BlazeComponent.extendComponent({
evt.stopPropagation(); evt.stopPropagation();
Sidebar.setView(); Sidebar.setView();
Filter.reset(); 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();
} }
}]; }];
} }

View file

@ -1,6 +1,10 @@
// We define a set of six board colors that we took from the FlatUI palette. // We define a set of six board colors that we took from the FlatUI palette.
// http://flatuicolors.com // 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) setBoardColor(color)
&#header, &#header,
&.sk-spinner div, &.sk-spinner div,
@ -8,13 +12,16 @@ setBoardColor(color)
.board-list & a .board-list & a
background-color: color background-color: color
& .minicard.is-selected .minicard-details .is-selected .minicard
border-left: 3px solid color border-left: 3px solid color
&.pop-over .pop-over-list li a:hover,
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%)
&.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 &#header #header-quick-access ul li.current
border-bottom: 2px solid lighten(color, 10%) border-bottom: 2px solid lighten(color, 10%)
@ -28,6 +35,17 @@ setBoardColor(color)
&:hover .board-header-btn-close &:hover .board-header-btn-close
background: darken(complement(color), 20%) 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 .board-color-nephritis
setBoardColor(#27AE60) setBoardColor(#27AE60)

View file

@ -19,7 +19,6 @@ Router.route('/boards/:_id/:slug', {
onAfterAction: function() { onAfterAction: function() {
// XXX We probably shouldn't rely on Session // XXX We probably shouldn't rely on Session
Session.set('sidebarIsOpen', true); Session.set('sidebarIsOpen', true);
Session.set('currentWidget', 'home');
Session.set('menuWidgetIsOpen', false); Session.set('menuWidgetIsOpen', false);
}, },
waitOn: function() { waitOn: function() {
@ -37,6 +36,7 @@ Router.route('/boards/:_id/:slug', {
Router.route('/boards/:boardId/:slug/:cardId', { Router.route('/boards/:boardId/:slug/:cardId', {
name: 'Card', name: 'Card',
template: 'board', template: 'board',
noEscapeActions: true,
onAfterAction: function() { onAfterAction: function() {
Tracker.nonreactive(function() { Tracker.nonreactive(function() {
if (! Session.get('currentCard') && Sidebar) { if (! Session.get('currentCard') && Sidebar) {
@ -57,7 +57,7 @@ Router.route('/boards/:boardId/:slug/:cardId', {
}); });
// Close the card details pane by pressing escape // Close the card details pane by pressing escape
EscapeActions.register('detailedPane', EscapeActions.register('detailsPane',
function() { return ! Session.equals('currentCard', null); }, function() { Utils.goBoardId(Session.get('currentBoard')); },
function() { Utils.goBoardId(Session.get('currentBoard')); } function() { return ! Session.equals('currentCard', null); }
); );

View file

@ -134,33 +134,6 @@
.card-composer .card-composer
padding-bottom: 8px 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 input[type="text"].attachment-add-link-input
float: left float: left
margin: 0 0 8px margin: 0 0 8px

View file

@ -19,6 +19,11 @@
&:hover &:hover
color: white color: white
&.square
height: 30px
width: @height
padding: 0
.card-label-green .card-label-green
background-color: #3cb500 background-color: #3cb500

View file

@ -1,7 +1,11 @@
template(name="minicard") template(name="minicard")
.minicard.card.js-minicard( a.minicard-wrapper.js-minicard(href=absoluteUrl
class="{{#if isSelected}}is-selected{{/if}}") class="{{#if isSelected}}is-selected{{/if}}"
a.minicard-details.clearfix.show(href=absoluteUrl) 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 if cover
.minicard-cover.js-card-cover(style="background-image: url({{cover.url}});") .minicard-cover.js-card-cover(style="background-image: url({{cover.url}});")
if labels if labels
@ -16,12 +20,12 @@ template(name="minicard")
.badges .badges
if comments.count if comments.count
.badge(title="{{_ 'card-comments-title' 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 .badge-text= comments.count
if description if description
.badge.badge-state-image-only(title=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 if attachments.count
.badge .badge
span.badge-icon.icon-sm.fa.fa-paperclip span.badge-icon.fa.fa-paperclip
span.badge-text= attachments.count span.badge-text= attachments.count

View file

@ -2,7 +2,6 @@
// 'click .member': Popup.open('cardMember') // 'click .member': Popup.open('cardMember')
// }); // });
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
template: function() { template: function() {
return 'minicard'; return 'minicard';
@ -10,5 +9,29 @@ BlazeComponent.extendComponent({
isSelected: function() { isSelected: function() {
return Session.equals('currentCard', this.currentData()._id); 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'); }).register('minicard');

View file

@ -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 .minicard
padding: 6px 8px 2px
position: relative
flex: 1
flex-wrap: wrap
background-color: #fff background-color: #fff
min-height: 20px
box-shadow: 0 1px 2px rgba(0,0,0,.2) box-shadow: 0 1px 2px rgba(0,0,0,.2)
border-radius: 2px border-radius: 2px
cursor: pointer color: #4d4d4d
margin-bottom: 9px
min-height: 20px
position: relative
z-index: 0
overflow: hidden overflow: hidden
transition: transform 0.2s, transition: transform 0.2s,
border-radius 0.2s, border-radius 0.2s,
border-left 0.2s border-left 0.2s
a .is-selected &
color: #4d4d4d transform: translateX(11px)
border-bottom-right-radius: 0
&.active-card border-top-right-radius: 0
background-color: #f0f0f0 z-index: 100
border-bottom-color: #c2c2c2 box-shadow: -2px 1px 2px rgba(0,0,0,.2)
.minicard-operation
display: block
&.draggable-hover-card
background-color: #f0f0f0
border-bottom-color: #c2c2c2
.minicard-cover .minicard-cover
background-position: center background-position: center
@ -39,21 +66,6 @@
background-size: auto background-size: auto
background-position: center 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 .minicard-details-overlay
background: transparent background: transparent
bottom: 0 bottom: 0
@ -121,23 +133,24 @@
.minicard-members:empty .minicard-members:empty
display: none display: none
&.ui-sortable-helper .badges
transform: rotate(4deg) float: left
.badges &:empty
float: left display: none
&:empty &.minicard-composer
display: none margin-bottom: 10px
textarea.minicard-composer-textarea, textarea.minicard-composer-textarea,
textarea.minicard-composer-textarea:focus textarea.minicard-composer-textarea:focus
background: none resize: none
border: none background: none
box-shadow: none border: none
height: auto box-shadow: none
margin-bottom: 4px height: auto
padding: 0 margin: 0
max-height: 162px padding: 0
min-height: 54px max-height: 162px
overflow-y: auto min-height: 54px
overflow-y: auto

View file

@ -1,8 +1,7 @@
template(name="cardMembersPopup") template(name="cardMembersPopup")
//- input.js-search-mem(autofocus placeholder="Search members…" type="text") ul.pop-over-member-list.js-mem-list
ul.pop-over-member-list.checkable.js-mem-list
each board.members 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="#") a.name.js-select-member(href="#")
+userAvatar(user=user size="small") +userAvatar(user=user size="small")
span.full-name span.full-name

View file

@ -30,10 +30,6 @@ input[type="radio"]
-webkit-appearance: radio -webkit-appearance: radio
min-height: inherit min-height: inherit
input[type="checkbox"]
-webkit-appearance: checkbox
margin-right: 4px
input[type="text"], input[type="text"],
input[type="password"], input[type="password"],
input[type="email"] input[type="email"]
@ -182,10 +178,6 @@ fieldset
input[type="hidden"] input[type="hidden"]
display: none display: none
input[type="checkbox"],
input[type="radio"]
display: inline
.radio-div, .radio-div,
.check-div .check-div
display: block display: block
@ -233,6 +225,36 @@ textarea
font-size: 26px font-size: 26px
margin: 3px 4px 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 .button-link
background: #fff background: #fff
background: linear-gradient(#fff, #f5f5f5) background: linear-gradient(#fff, #f5f5f5)
@ -355,9 +377,6 @@ textarea
background-color: rgba(255, 255, 255, .3) background-color: rgba(255, 255, 255, .3)
border-color: transparent border-color: transparent
.icon-sm
color: #fff
&:active &:active
background: #2e85b8 background: #2e85b8
background: linear-gradient(#2e85b8, #28739f) background: linear-gradient(#2e85b8, #28739f)
@ -401,7 +420,6 @@ textarea
border-color: #8b0e0e border-color: #8b0e0e
button button
&.quiet-button, &.quiet-button,
&.loud-text-button &.loud-text-button
background: none background: none
@ -438,11 +456,6 @@ button
&.w-img &.w-img
padding-left: 28px padding-left: 28px
.icon-sm
left: 6px
position: absolute
top: 6px
&:hover &:hover
color: #4d4d4d color: #4d4d4d
background: #dcdcdc background: #dcdcdc
@ -575,29 +588,8 @@ button
border-color: #2e85b8 border-color: #2e85b8
color: #fff 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 .dropdown-menu
border-radius: 2px border-radius: 2px
// padding-bottom: 3px
overflow: hidden overflow: hidden
li li

View file

@ -97,6 +97,6 @@ BlazeComponent.extendComponent({
// Press escape to close the currently opened inlinedForm // Press escape to close the currently opened inlinedForm
EscapeActions.register('inlinedForm', EscapeActions.register('inlinedForm',
function() { return currentlyOpenedForm.get() !== null; }, function() { currentlyOpenedForm.get().close(); },
function() { currentlyOpenedForm.get().close(); } function() { return currentlyOpenedForm.get() !== null; }
); );

View file

@ -10,13 +10,12 @@ template(name="listBody")
+inlinedForm(autoclose=false position="bottom") +inlinedForm(autoclose=false position="bottom")
+addCardForm(listId=_id position="bottom") +addCardForm(listId=_id position="bottom")
else else
if newCardFormIsVisible.get a.open-minicard-composer.js-open-inlined-form
a.open-card-composer.js-open-inlined-form i.fa.fa-plus
i.fa.fa-plus | {{_ 'add-card'}}
| {{_ 'add-card'}}
template(name="addCardForm") template(name="addCardForm")
.minicard.js-composer .minicard.minicard-composer.js-composer
.minicard-labels.js-minicard-composer-labels .minicard-labels.js-minicard-composer-labels
.minicard-details.clearfix .minicard-details.clearfix
textarea.minicard-composer-textarea.js-card-title(autofocus) textarea.minicard-composer-textarea.js-card-title(autofocus)

View file

@ -34,18 +34,17 @@ BlazeComponent.extendComponent({
} }
if ($.trim(title)) { if ($.trim(title)) {
Cards.insert({ var _id = Cards.insert({
title: title, title: title,
listId: this.data()._id, listId: this.data()._id,
boardId: this.data().board()._id, boardId: this.data().board()._id,
sort: sortIndex 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. // We keep the form opened, empty it, and scroll to it.
textarea.val('').focus(); textarea.val('').focus();
@ -55,10 +54,6 @@ BlazeComponent.extendComponent({
} }
}, },
showNewCardForm: function(value) {
this.newCardFormIsVisible.set(value);
},
scrollToBottom: function() { scrollToBottom: function() {
var container = this.firstNode(); var container = this.firstNode();
$(container).animate({ $(container).animate({
@ -66,14 +61,10 @@ BlazeComponent.extendComponent({
}); });
}, },
onCreated: function() {
this.newCardFormIsVisible = new ReactiveVar(true);
},
events: function() { events: function() {
return [{ return [{
submit: this.addCard, submit: this.addCard,
'click .open-card-composer': this.scrollToBottom 'click .open-minicard-composer': this.scrollToBottom
}]; }];
} }
}).register('listBody'); }).register('listBody');

View file

@ -8,10 +8,6 @@ BlazeComponent.extendComponent({
this.componentChildren('listBody')[0].openForm(options); this.componentChildren('listBody')[0].openForm(options);
}, },
showNewCardForm: function(value) {
this.componentChildren('listBody')[0].showNewCardForm(value);
},
onCreated: function() { onCreated: function() {
this.newCardFormIsVisible = new ReactiveVar(true); this.newCardFormIsVisible = new ReactiveVar(true);
}, },
@ -35,30 +31,59 @@ BlazeComponent.extendComponent({
connectWith: '.js-minicards', connectWith: '.js-minicards',
tolerance: 'pointer', tolerance: 'pointer',
appendTo: '.js-lists', appendTo: '.js-lists',
helper: 'clone', helper: function(evt, item) {
items: itemsSelector, var helper = item.clone();
placeholder: 'minicard placeholder', if (MultiSelection.isActive()) {
start: function(event, ui) { var andNOthers = $cards.find('.js-minicard.is-checked').length - 1;
ui.placeholder.height(ui.helper.height()); if (andNOthers > 0) {
Popup.close(); helper.append($(Blaze.toHTML(HTML.DIV(
boardComponent.showNewCardForms(false); // XXX Super bad class name
{'class': 'and-n-other'},
// XXX Need to translate
'and ' + andNOthers + ' other cards.'
))));
}
}
return helper;
}, },
stop: function(event, ui) { items: itemsSelector,
placeholder: 'minicard-wrapper placeholder',
start: function(evt, ui) {
ui.placeholder.height(ui.helper.height());
EscapeActions.executeLowerThan('popup');
boardComponent.setIsDragging(true);
},
stop: function(evt, ui) {
// To attribute the new index number, we need to get the dom element // To attribute the new index number, we need to get the dom element
// of the previous and the following card -- if any. // of the previous and the following card -- if any.
var cardDomElement = ui.item.get(0); var cardDomElement = ui.item.get(0);
var prevCardDomElement = ui.item.prev('.js-minicard').get(0); var prevCardDomElement = ui.item.prev('.js-minicard').get(0);
var nextCardDomElement = ui.item.next('.js-minicard').get(0); var nextCardDomElement = ui.item.next('.js-minicard').get(0);
var sort = Utils.getSortIndex(prevCardDomElement, nextCardDomElement); var sort = Utils.getSortIndex(prevCardDomElement, nextCardDomElement);
var cardId = Blaze.getData(cardDomElement)._id;
var listId = Blaze.getData(ui.item.parents('.list').get(0))._id; var listId = Blaze.getData(ui.item.parents('.list').get(0))._id;
Cards.update(cardId, {
$set: { if (MultiSelection.isActive()) {
listId: listId, Cards.find(MultiSelection.getMongoSelector()).forEach(function(c) {
sort: sort Cards.update(c._id, {
} $set: {
}); listId: listId,
boardComponent.showNewCardForms(true); 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);
} }
}); });

View file

@ -93,10 +93,13 @@
overflow-y: auto overflow-y: auto
padding: 5px 11px padding: 5px 11px
.minicards form
margin-bottom: 9px
.ps-scrollbar-y-rail .ps-scrollbar-y-rail
transform: translateX(2px) transform: translateX(2px)
.open-card-composer .open-minicard-composer
border-radius: 2px border-radius: 2px
color: #8c8c8c color: #8c8c8c
display: block display: block

View file

@ -5,6 +5,7 @@ template(name="listActionPopup")
if cards.count if cards.count
hr hr
ul.pop-over-list ul.pop-over-list
li: a.js-select-cards {{_ 'list-select-cards'}}
li: a.js-move-cards {{_ 'list-move-cards'}} li: a.js-move-cards {{_ 'list-move-cards'}}
li: a.js-archive-cards {{_ 'list-archive-cards'}} li: a.js-archive-cards {{_ 'list-archive-cards'}}
hr hr

View file

@ -6,6 +6,14 @@ Template.listActionPopup.events({
Popup.close(); Popup.close();
}, },
'click .js-list-subscribe': function() {}, '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-move-cards': Popup.open('listMoveCards'),
'click .js-archive-cards': Popup.afterConfirm('listArchiveCards', function() { 'click .js-archive-cards': Popup.afterConfirm('listArchiveCards', function() {
Cards.find({listId: this._id}).forEach(function(card) { Cards.find({listId: this._id}).forEach(function(card) {

View file

@ -61,6 +61,6 @@ Template.editor.onRendered(function() {
}); });
EscapeActions.register('textcomplete', EscapeActions.register('textcomplete',
function() { return dropdownMenuIsOpened; }, function() {},
function() {} function() { return dropdownMenuIsOpened; }
); );

View file

@ -58,6 +58,9 @@
margin: 4px 8px 0 0 margin: 4px 8px 0 0
float: left float: left
i.fa-chevron-down
margin-right: 4px
#header-main-bar #header-main-bar
height: 28px * 1.618034 - 6px height: 28px * 1.618034 - 6px
padding: 7px 10px 0 padding: 7px 10px 0

View file

@ -35,21 +35,9 @@
margin: 4px 0 12px margin: 4px 0 12px
width: 100% width: 100%
.empty
margin: 0
img img
max-width: 270px max-width: 270px
.custom-image img
height: 18px
left: 9px
top: 9px
width: 18px
.title
line-height: 32px
.header .header
height: 36px height: 36px
position: relative position: relative
@ -68,10 +56,6 @@
text-overflow: ellipsis text-overflow: ellipsis
white-space: nowrap white-space: nowrap
.back-btn, .close-btn
&:hover .icon-sm
color: darken(white, 80%)
.back-btn .back-btn
float: left float: left
overflow: hidden overflow: hidden
@ -91,7 +75,6 @@
top: 0 top: 0
right: 0 right: 0
&.no-title .header &.no-title .header
background: none background: none
@ -134,15 +117,11 @@
margin-bottom: 8px margin-bottom: 8px
.pop-over-list .pop-over-list
&.navigable li.not-selectable>a:hover, &.navigable li.not-selectable>a:hover,
li.not-selectable>a:hover li.not-selectable>a:hover
color: #8c8c8c color: #8c8c8c
cursor: default cursor: default
.icon-sm
color: #a6a6a6
li > a li > a
cursor: pointer cursor: pointer
display: block display: block
@ -168,9 +147,6 @@
.unread-indicator .unread-indicator
background: #fff background: #fff
.icon-sm
color: #fff
.sub-name .sub-name
clear: both clear: both
color: #8c8c8c color: #8c8c8c
@ -208,9 +184,6 @@
.vis-icon .vis-icon
opacity: .35 opacity: .35
.icon-sm
color: #a6a6a6
&:hover &:hover
background: none background: none
@ -218,9 +191,6 @@
.quiet .quiet
color: #8c8c8c color: #8c8c8c
.icon-sm
color: #a6a6a6
&:active &:active
background: none background: none
@ -268,9 +238,6 @@
.quiet .quiet
color: #8c8c8c color: #8c8c8c
.icon-sm
color: #a6a6a6
li.selected > a li.selected > a
background-color: #005377 background-color: #005377
color: #fff color: #fff
@ -287,14 +254,10 @@
.unread-indicator .unread-indicator
background: #fff background: #fff
.icon-sm
color: #fff
&:active &:active
background-color: #005377 background-color: #005377
.pop-over.miniprofile .pop-over.miniprofile
.header .header
border-bottom-color: transparent border-bottom-color: transparent
height: 30px height: 30px
@ -329,205 +292,3 @@
&:hover &:hover
text-decoration: underline 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

View file

@ -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) { var getMemberIndex = function(board, searchId) {
for (var i = 0; i < board.members.length; i++) { for (var i = 0; i < board.members.length; i++) {
if (board.members[i].userId === searchId) if (board.members[i].userId === searchId)

View file

@ -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({ // Template.addMemberPopup.helpers({
// isBoardMember: function() { // isBoardMember: function() {
// var user = Users.findOne(this._id); // var user = Users.findOne(this._id);

View file

@ -4,49 +4,22 @@ template(name="sidebar")
class="{{#if isTongueHidden}}is-hidden{{/if}}") class="{{#if isTongueHidden}}is-hidden{{/if}}")
i.fa.fa-chevron-left i.fa.fa-chevron-left
.sidebar-content.js-board-sidebar-content.js-perfect-scrollbar .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.dynamic(template=getViewTemplate)
template(name='homeSidebar') template(name='homeSidebar')
+membersWidget +membersWidget
hr.clear hr
+labelsWidget +labelsWidget
hr.clear hr
h3 h3
i.fa.fa-comments-o i.fa.fa-comments-o
| {{_ 'activities'}} | {{_ 'activities'}}
+activities(mode="board") +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
| (<span class="username">{{ username }}</span>)
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") template(name="membersWidget")
.board-widget.board-widget-members .board-widget.board-widget-members
h3 h3

View file

@ -1,6 +1,11 @@
Sidebar = null;
var defaultView = 'home'; var defaultView = 'home';
Sidebar = null; var viewTitles = {
filter: 'filter-cards',
multiselection: 'multi-selection'
};
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
template: function() { template: function() {
@ -60,14 +65,23 @@ BlazeComponent.extendComponent({
}, },
setView: function(view) { setView: function(view) {
view = view || defaultView; view = _.isString(view) ? view : defaultView;
this._view.set(view); this._view.set(view);
this.open();
},
isDefaultView: function() {
return this.getView() === defaultView;
}, },
getViewTemplate: function() { getViewTemplate: function() {
return this.getView() + 'Sidebar'; return this.getView() + 'Sidebar';
}, },
getViewTitle: function() {
return TAPi18n.__(viewTitles[this.getView()]);
},
// Board members can assign people or labels by drag-dropping elements from // 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 // 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 // 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` // XXX Hacky, we need some kind of `super`
var mixinEvents = this.getMixin(Mixins.InfiniteScrolling).events(); var mixinEvents = this.getMixin(Mixins.InfiniteScrolling).events();
return mixinEvents.concat([{ return mixinEvents.concat([{
'click .js-toogle-sidebar': this.toogle 'click .js-toogle-sidebar': this.toogle,
'click .js-back-home': this.setView
}]); }]);
} }
}).register('sidebar'); }).register('sidebar');
EscapeActions.register('sidebarView', EscapeActions.register('sidebarView',
function() { return Sidebar && Sidebar.getView() !== defaultView; }, function() { Sidebar.setView(defaultView); },
function() { Sidebar.setView(defaultView); } function() { return Sidebar && Sidebar.getView() !== defaultView; }
); );

View file

@ -7,7 +7,7 @@
right: 0 right: 0
.sidebar-content .sidebar-content
padding: 10px 20px padding: 12px
background: white background: white
box-shadow: -10px 0px 5px -10px darken(white, 30%) box-shadow: -10px 0px 5px -10px darken(white, 30%)
z-index: 10 z-index: 10
@ -23,7 +23,33 @@
color: darken(white, 50%) color: darken(white, 50%)
hr 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 .board-sidebar
width: 248px width: 248px

View file

@ -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
| (<span class="username">{{ username }}</span>)
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

View file

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

View file

@ -0,0 +1,77 @@
<!-- XXX Translate these template into jade -->
<template name="closeBoardPopup">
<p>{{_ 'close-board-pop'}}</p>
<input type="submit" class="js-confirm negate full" value="{{_ 'close'}}">
</template>
<template name="removeMemberPopup">
<p>{{_ 'remove-member-pop'
name=user.profile.name
username=user.username
boardTitle=board.title}}</p>
<input type="submit" class="js-confirm negate full" value="{{_ 'remove-member'}}">
</template>
<template name="addMemberPopup">
<div class="search-with-spinner">
{{> esInput index="users" }}
</div>
<div class="manage-member-section hide js-search-results" style="display: block;">
<ul class="pop-over-member-list options js-list">
{{# esEach index="users"}}
<li class="item js-member-item {{# if isBoardMember }}disabled{{/if}}">
<a href="#" class="name js-select-member {{# if isBoardMember }}multi-line{{/if}}" title="{{ profile.name }} ({{ username }})">
{{> userAvatar user=this size="small" }}
<span class="full-name">
{{ profile.name }} (<span class="username">{{ username }}</span>)
</span>
{{# if isBoardMember }}
<div class="extra-text quiet">({{_ 'joined'}})</div>
{{/if}}
<span class="icon-sm fa fa-chevron-right light option js-open-option"></span>
</a>
</li>
{{/esEach }}
</ul>
</div>
{{# ifEsIsSearching index='users' }}
<div class="tac">
<span class="tabbed-pane-main-col-loading-spinner spinner"></span>
</div>
{{ /ifEsIsSearching }}
{{# ifEsHasNoResults index="users" }}
<div class="manage-member-section js-no-results">
<p class="quiet center" style="padding: 16px 4px;">{{_ 'no-results'}}</p>
</div>
{{ /ifEsHasNoResults }}
<div class="manage-member-section js-helper">
<p class="bottom quiet" style="padding: 6px;">{{_ 'search-member-desc'}}</p>
</div>
</template>
<template name="changePermissionsPopup">
<ul class="pop-over-list">
<li>
<a class="{{#if isLastAdmin}}disabled{{else}}js-set-admin{{/if}}">
{{_ 'admin'}}
{{#if isAdmin}}<span class="icon-sm fa fa-check"></span>{{/if}}
<span class="sub-name">{{_ 'admin-desc'}}</span>
</a>
</li>
<li>
<a class="{{#if isLastAdmin}}disabled{{else}}js-set-normal{{/if}}">
{{_ 'normal'}}
{{#unless isAdmin}}<span class="icon-sm fa fa-check"></span>{{/unless}}
<span class="sub-name">{{_ 'normal-desc'}}</span>
</a>
</li>
</ul>
{{#if isLastAdmin}}
<hr>
<p class="quiet bottom">{{_ 'last-admin-desc'}}</p>
{{/if}}
</template>

View file

@ -1,307 +0,0 @@
<template name="boardWidgets">
<a href="#" class="sidebar-show-btn dark-hover js-show-sidebar">
<span class="icon-sm fa fa-chevron-left"></span>
<span class="text">{{_ 'show-sidebar'}}</span>
</a>
<div class="board-widgets {{#if session 'sidebarIsOpen'}}show{{else}}hide{{/if}}">
<div>
<a href="#" class="sidebar-hide-btn dark-hover js-hide-sidebar" title="{{_ 'close-sidebar-title'}}">
<span class="icon-sm fa fa-chevron-right"></span>
</a>
{{#unless isTrue currentWidget "homeWidget"}}
<div class="board-widgets-title clearfix">
<a href="#" class="board-sidebar-back-btn js-pop-widget-view">
<span class="left-arrow"></span>{{_ 'back'}}
</a>
<h3 class="text">{{currentWidgetTitle}}</h3>
<hr>
</div>
{{/unless}}
<div class="board-widgets-content-wrapper">
<div class="board-widgets-content default fancy-scrollbar short{{#unless session 'menuWidgetIsOpen'}} short{{/unless}}">
{{> UI.dynamic template=currentWidget data=this }}
</div>
</div>
</div>
</div>
</template>
<template name="homeWidget">
{{ > menuWidget }}
{{ > membersWidget }}
{{ > activityWidget }}
</template>
<template name="menuWidget">
<div class="board-widget board-widget-nav clearfix{{#unless session 'menuWidgetIsOpen'}} collapsed{{/unless}}">
<h3 class="dark-hover toggle-widget-nav js-toggle-widget-nav">{{_ 'menu'}}
<span class="icon-sm fa fa-chevron-circle-down toggle-menu-icon"></span>
</h3>
<ul class="nav-list">
<hr style="margin-top: 0;">
<li>
<a href="#" class="nav-list-item js-open-archive">
<span class="icon-sm fa fa-archive icon-type"></span>
{{_ 'archived-items'}}
</a>
</li>
<li>
<a href="#" class="nav-list-item js-open-card-filter">
<span class="icon-sm fa fa-filter icon-type"></span>
{{_ 'filter-cards'}}
</a>
</li>
{{#if currentUser.isBoardAdmin}}
<hr>
<li>
<a class="nav-list-item nav-list-sub-item board-settings-background js-change-background">
<span class="board-settings-background-preview" style="background-color:{{board.background.color}}"></span>
{{_ 'change-background'}}…
</a>
</li>
{{#unless isSandstorm }}
<li>
<a class="nav-list-item nav-list-sub-item js-close-board" href="#">{{_ 'close-board'}}</a>
</li>
{{/unless}}
{{/if}}
{{!
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}}
<hr>
<li>
<a class="nav-list-item nav-list-sub-item js-language">{{_ 'language'}}</a>
</li>
{{/if}}
</ul>
</div>
</template>
<template name="membersWidget">
<hr>
<div class="board-widget board-widget-members clearfix">
<div class="board-widget-title">
<h3>{{_ 'members'}}</h3>
</div>
<div class="board-widget-content">
<div class="board-widget-members js-list-board-members clearfix js-list-draggable-board-members">
{{# each board.members }}
{{> userAvatar userId=this.userId draggable=true size="small" showBadges=true}}
{{/ each }}
</div>
{{# unless isSandstrom }}
{{# if currentUser.isBoardAdmin }}
<a href="#" class="button-link js-open-manage-board-members">
<span class="icon-sm fa fa-user"></span> {{_ 'add-members'}}
</a>
{{/ if }}
{{/ unless }}
</div>
</div>
</template>
<template name="activityWidget">
{{# if board.activities.count }}
<hr>
<div class="board-widget board-widget-activity bottom clearfix">
<div class="board-widget-title">
<h3>{{_ 'activity'}}</h3>
</div>
<div class="board-widget-content">
<div class="activity-gradient-t"></div>
<div class="activity-gradient-b"></div>
<div class="board-actions-list fancy-scrollbar">
{{ > activities }}
</div>
</div>
</div>
{{/if}}
</template>
<template name="memberPopup">
<div class="board-member-menu">
<div class="mini-profile-info">
{{> userAvatar user=user}}
<div class="info">
<h3 class="bottom" style="margin-right: 40px;">
<a class="js-profile" href="{{ pathFor route='Profile' username=user.username }}">{{ user.profile.name }}</a>
</h3>
<p class="quiet bottom">@{{ user.username }}</p>
</div>
</div>
{{# if currentUser.isBoardMember }}
<ul class="pop-over-list">
{{# if currentUser.isBoardAdmin }}
<li>
<a class="js-change-role" href="#">
{{_ 'change-permissions'}} <span class="quiet" style="font-weight: normal;">({{ memberType }})</span>
</a>
</li>
{{/ if }}
<li>
{{# if currentUser.isBoardAdmin }}
<a class="js-remove-member">{{_ 'remove-from-board'}}</a>
{{ else }}
<a class="js-leave-member">{{_ 'leave-board'}}</a>
{{/ if }}
</li>
</ul>
{{/ if }}
</div>
</template>
<template name="filterWidget">
<ul class="pop-over-label-list checkable">
{{#each board.labels}}
<li class="item matches-filter">
<a class="name js-toggle-label-filter">
<span class="card-label card-label-{{color}}"></span>
<span class="full-name">
{{#if name}}
{{name}}
{{else}}
<span class="quiet">{{_ "label-default" color}}</span>
{{/if}}
</span>
{{#if Filter.labelIds.isSelected _id}}
<span class="icon-sm fa fa-check"></span>
{{/if}}
</a>
</li>
{{/each}}
</ul>
<hr>
<ul class="pop-over-member-list checkable">
{{#each board.members}}
{{#with getUser userId}}
<li class="item js-member-item {{#if Filter.members.isSelected _id}}active{{/if}}">
<a href="#" class="name js-toogle-member-filter">
{{> userAvatar user=this size="small" }}
<span class="full-name">
{{ profile.name }}
(<span class="username">{{ username }}</span>)
</span>
{{#if Filter.members.isSelected _id}}
<span class="icon-sm fa fa-check checked-icon"></span>
{{/if}}
</a>
</li>
{{/with}}
{{/each}}
</ul>
<hr>
<ul class="pop-over-list inset normal-weight">
<li>
<a class="js-clear-all {{#unless Filter.isActive}}disabled{{/unless}}" style="padding-left: 40px;">
{{_ 'filter-clear'}}
</a>
</li>
</ul>
</template>
<template name="backgroundWidget">
<div class="board-widgets-content-wrapper fancy-scrollbar">
<div class="board-widgets-content">
<div class="board-backgrounds-list clearfix">
{{#each backgroundColors}}
<div class="board-background-select js-select-background">
<span class="background-box " style="background-color: {{this}}; "></span>
</div>
{{/each}}
</div>
{{!--
<h2 class="clear">Photos</h2>
<div class="board-backgrounds-list relative clearfix js-gold-photos-list disabled">
<div class="board-background-select js-select-background">
<span class="background-box " style="background-image: url(&quot;{{url}}&quot;);">
<a class="background-option js-background-attribution" href={{href}} target="_blank" title={{title}}>
<img src="https://d78fikflryjgj.cloudfront.net/images/d906fe5c1274c56c5571d49705547587/cc.png" style="height: 14px; width: 14px; vertical-align: text-top;" title="http://creativecommons.org/licenses/by/2.0/deed.en">
<span class="text" style="margin-left: 2px;">{{author}}</span>
</a>
</span>
</div>
</div>
--}}
</div>
</div>
</template>
<template name="closeBoardPopup">
<p>{{_ 'close-board-pop'}}</p>
<input type="submit" class="js-confirm negate full" value="{{_ 'close'}}">
</template>
<template name="removeMemberPopup">
<p>{{_ 'remove-member-pop'
name=user.profile.name
username=user.username
boardTitle=board.title}}</p>
<input type="submit" class="js-confirm negate full" value="{{_ 'remove-member'}}">
</template>
<template name="addMemberPopup">
<div class="search-with-spinner">
{{> esInput index="users" }}
</div>
<div class="manage-member-section hide js-search-results" style="display: block;">
<ul class="pop-over-member-list options js-list">
{{# esEach index="users"}}
<li class="item js-member-item {{# if isBoardMember }}disabled{{/if}}">
<a href="#" class="name js-select-member {{# if isBoardMember }}multi-line{{/if}}" title="{{ profile.name }} ({{ username }})">
{{> userAvatar user=this size="small" }}
<span class="full-name">
{{ profile.name }} (<span class="username">{{ username }}</span>)
</span>
{{# if isBoardMember }}
<div class="extra-text quiet">({{_ 'joined'}})</div>
{{/if}}
<span class="icon-sm fa fa-chevron-right light option js-open-option"></span>
</a>
</li>
{{/esEach }}
</ul>
</div>
{{# ifEsIsSearching index='users' }}
<div class="tac">
<span class="tabbed-pane-main-col-loading-spinner spinner"></span>
</div>
{{ /ifEsIsSearching }}
{{# ifEsHasNoResults index="users" }}
<div class="manage-member-section js-no-results">
<p class="quiet center" style="padding: 16px 4px;">{{_ 'no-results'}}</p>
</div>
{{ /ifEsHasNoResults }}
<div class="manage-member-section js-helper">
<p class="bottom quiet" style="padding: 6px;">{{_ 'search-member-desc'}}</p>
</div>
</template>
<template name="changePermissionsPopup">
<ul class="pop-over-list">
<li>
<a class="{{#if isLastAdmin}}disabled{{else}}js-set-admin{{/if}}">
{{_ 'admin'}}
{{#if isAdmin}}<span class="icon-sm fa fa-check"></span>{{/if}}
<span class="sub-name">{{_ 'admin-desc'}}</span>
</a>
</li>
<li>
<a class="{{#if isLastAdmin}}disabled{{else}}js-set-normal{{/if}}">
{{_ 'normal'}}
{{#unless isAdmin}}<span class="icon-sm fa fa-check"></span>{{/unless}}
<span class="sub-name">{{_ 'normal-desc'}}</span>
</a>
</li>
</ul>
{{#if isLastAdmin}}
<hr>
<p class="quiet bottom">{{_ 'last-admin-desc'}}</p>
{{/if}}
</template>

View file

@ -1,3 +1,6 @@
// XXX Switch to Flow-Router?
var previousRoute;
Router.configure({ Router.configure({
loadingTemplate: 'spinner', loadingTemplate: 'spinner',
notFoundTemplate: 'notfound', notFoundTemplate: 'notfound',
@ -6,24 +9,43 @@ Router.configure({
onBeforeAction: function() { onBeforeAction: function() {
var options = this.route.options; var options = this.route.options;
var loggedIn = Tracker.nonreactive(function() {
return !! Meteor.userId();
});
// Redirect logged in users to Boards view when they try to open Login or // Redirect logged in users to Boards view when they try to open Login or
// signup views. // signup views.
if (Meteor.userId() && options.redirectLoggedInUsers) { if (loggedIn && options.redirectLoggedInUsers) {
return this.redirect('Boards'); return this.redirect('Boards');
} }
// Authenticated // Authenticated
if (! Meteor.userId() && options.authenticated) { if (! loggedIn && options.authenticated) {
return this.redirect('atSignIn'); return this.redirect('atSignIn');
} }
// Reset default sessions
Session.set('error', false);
Tracker.nonreactive(function() { Tracker.nonreactive(function() {
EscapeActions.executeLowerThan(40); if (! options.noEscapeActions &&
! (previousRoute && previousRoute.options.noEscapeActions))
EscapeActions.executeAll();
}); });
previousRoute = this.route;
this.next(); this.next();
} }
}); });
// We want to execute our EscapeActions.executeLowerThan method any time the
// route is changed, but not if the stays the same but only the parameters
// change (eg when a user is navigation from a card A to a card B). This is why
// we cant put this function in the above `onBeforeAction` that is being run
// too many times, instead we register a dependency only on the route name and
// use Tracker.autorun. The following paragraph explains the problem quite well:
// https://github.com/meteorhacks/flow-router#routercurrent-is-evil
// Tracker.autorun(function(computation) {
// routeName.get();
// if (! computation.firstRun) {
// EscapeActions.executeLowerThan('inlinedForm');
// }
// });

View file

@ -91,7 +91,7 @@ Filter = {
}); });
}, },
getMongoSelector: function() { _getMongoSelector: function() {
var self = this; var self = this;
if (! self.isActive()) if (! self.isActive())
@ -110,6 +110,14 @@ Filter = {
return {$or: [filterSelector, exceptionsSelector]}; return {$or: [filterSelector, exceptionsSelector]};
}, },
mongoSelector: function(additionalSelector) {
var filterSelector = this._getMongoSelector();
if (_.isUndefined(additionalSelector))
return filterSelector;
else
return {$and: [filterSelector, additionalSelector]};
},
reset: function() { reset: function() {
var self = this; var self = this;
_.forEach(self._fields, function(fieldName) { _.forEach(self._fields, function(fieldName) {
@ -123,6 +131,7 @@ Filter = {
if (this.isActive()) { if (this.isActive()) {
this._exceptions.push(_id); this._exceptions.push(_id);
this._exceptionsDep.changed(); this._exceptionsDep.changed();
Tracker.flush();
} }
}, },

View file

@ -47,11 +47,16 @@ EscapeActions = {
'textcomplete', 'textcomplete',
'popup', 'popup',
'inlinedForm', 'inlinedForm',
'multiselection-disable',
'sidebarView', 'sidebarView',
'detailedPane' 'detailsPane',
'multiselection-reset'
], ],
register: function(label, condition, action) { register: function(label, action, condition) {
if (_.isUndefined(condition))
condition = function() { return true; };
// XXX Rewrite this with ES6: .push({ priority, condition, action }) // XXX Rewrite this with ES6: .push({ priority, condition, action })
var priority = this.hierarchy.indexOf(label); var priority = this.hierarchy.indexOf(label);
if (priority === -1) { if (priority === -1) {
@ -87,6 +92,10 @@ EscapeActions = {
if (!! currentAction.condition()) if (!! currentAction.condition())
currentAction.action(); currentAction.action();
} }
},
executeAll: function() {
return this.executeLowerThan();
} }
}; };

View file

@ -0,0 +1,159 @@
var getCardsBetween = function(idA, idB) {
var pluckId = function(doc) {
return doc._id;
};
var getListsStrictlyBetween = function(id1, id2) {
return Lists.find({
$and: [
{ sort: { $gt: Lists.findOne(id1).sort } },
{ sort: { $lt: Lists.findOne(id2).sort } }
],
archived: false
}).map(pluckId);
};
var cards = _.sortBy([Cards.findOne(idA), Cards.findOne(idB)], function(c) {
return c.sort;
});
var selector;
if (cards[0].listId === cards[1].listId) {
selector = {
listId: cards[0].listId,
sort: {
$gte: cards[0].sort,
$lte: cards[1].sort
},
archived: false
};
} else {
selector = {
$or: [{
listId: cards[0].listId,
sort: { $lte: cards[0].sort }
}, {
listId: {
$in: getListsStrictlyBetween(cards[0].listId, cards[1].listId)
}
}, {
listId: cards[1].listId,
sort: { $gte: cards[1].sort }
}],
archived: false
};
}
return Cards.find(Filter.mongoSelector(selector)).map(pluckId);
};
MultiSelection = {
sidebarView: 'multiselection',
_selectedCards: new ReactiveVar([]),
_isActive: new ReactiveVar(false),
startRangeCardId: null,
reset: function() {
this._selectedCards.set([]);
},
getMongoSelector: function() {
return Filter.mongoSelector({
_id: { $in: this._selectedCards.get() }
});
},
isActive: function() {
return this._isActive.get();
},
isEmpty: function() {
return this._selectedCards.get().length === 0;
},
activate: function() {
if (! this.isActive()) {
EscapeActions.executeLowerThan('detailsPane');
this._isActive.set(true);
Sidebar.setView(this.sidebarView);
Tracker.flush();
}
},
disable: function() {
if (this.isActive()) {
this._isActive.set(false);
if (Sidebar && Sidebar.getView() === this.sidebarView) {
Sidebar.setView();
}
}
},
add: function(cardIds) {
return this.toogle(cardIds, { add: true, remove: false });
},
remove: function(cardIds) {
return this.toogle(cardIds, { add: false, remove: true });
},
toogleRange: function(cardId) {
var selectedCards = this._selectedCards.get();
var startRange;
this.reset();
if (! this.isActive() || selectedCards.length === 0) {
this.toogle(cardId);
} else {
startRange = selectedCards[selectedCards.length - 1];
this.toogle(getCardsBetween(startRange, cardId));
}
},
toogle: function(cardIds, options) {
var self = this;
cardIds = _.isString(cardIds) ? [cardIds] : cardIds;
options = _.extend({
add: true,
remove: true
}, options || {});
if (! self.isActive()) {
self.reset();
self.activate();
}
var selectedCards = self._selectedCards.get();
_.each(cardIds, function(cardId) {
var indexOfCard = selectedCards.indexOf(cardId);
if (options.remove && indexOfCard > -1)
selectedCards.splice(indexOfCard, 1);
else if (options.add)
selectedCards.push(cardId);
});
self._selectedCards.set(selectedCards);
},
isSelected: function(cardId) {
return this._selectedCards.get().indexOf(cardId) > -1;
}
};
Blaze.registerHelper('MultiSelection', MultiSelection);
EscapeActions.register('multiselection-disable',
function() { MultiSelection.disable(); },
function() { return MultiSelection.isActive(); }
);
EscapeActions.register('multiselection-reset',
function() { MultiSelection.reset(); }
);

View file

@ -205,6 +205,6 @@ $(document).on('click', function(evt) {
// Press escape to close the popup. // Press escape to close the popup.
var bindPopup = function(f) { return _.bind(f, Popup); }; var bindPopup = function(f) { return _.bind(f, Popup); };
EscapeActions.register('popup', EscapeActions.register('popup',
bindPopup(Popup.isOpen), bindPopup(Popup.close),
bindPopup(Popup.close) bindPopup(Popup.isOpen)
); );

View file

@ -318,44 +318,6 @@ dd
.card-composer .card-composer
padding-bottom: 8px 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
width: 80%
input[type="submit"].attachment-add-link-submit
float: left
margin: 0 0 8px 4px
padding: 6px 12px
width: 18%
.card-detail-badge .card-detail-badge
background-color: #dbdbdb background-color: #dbdbdb
border-radius: 3px border-radius: 3px

View file

@ -120,9 +120,15 @@ Cards.helpers({
}); });
return cardLabels; return cardLabels;
}, },
hasLabel: function(labelId) {
return _.contains(this.labelIds, labelId);
},
user: function() { user: function() {
return Users.findOne(this.userId); return Users.findOne(this.userId);
}, },
isAssigned: function(memberId) {
return _.contains(this.members, memberId);
},
activities: function() { activities: function() {
return Activities.find({ type: 'card', cardId: this._id }, return Activities.find({ type: 'card', cardId: this._id },
{ sort: { createdAt: -1 }}); { sort: { createdAt: -1 }});

View file

@ -44,7 +44,7 @@ if (Meteor.isServer) {
Lists.helpers({ Lists.helpers({
cards: function() { cards: function() {
return Cards.find(_.extend(Filter.getMongoSelector(), { return Cards.find(Filter.mongoSelector({
listId: this._id, listId: this._id,
archived: false archived: false
}), { sort: ['sort'] }); }), { sort: ['sort'] });

View file

@ -74,7 +74,7 @@
"email-placeholder": "e.g., doc@frankenstein.com", "email-placeholder": "e.g., doc@frankenstein.com",
"filter": "Filter", "filter": "Filter",
"filter-cards": "Filter Cards", "filter-cards": "Filter Cards",
"filter-clear": "Clear filter.", "filter-clear": "Clear filter",
"filter-on": "Filter is on", "filter-on": "Filter is on",
"filter-on-desc": "You are filtering cards on this board. Click here to edit filter.", "filter-on-desc": "You are filtering cards on this board. Click here to edit filter.",
"fullname": "Full Name", "fullname": "Full Name",
@ -98,6 +98,7 @@
"leave-board": "Leave Board…", "leave-board": "Leave Board…",
"link-card": "Link to this card", "link-card": "Link to this card",
"list-move-cards": "Move All Cards in This List…", "list-move-cards": "Move All Cards in This List…",
"list-select-cards": "Select All Cards in This List",
"list-archive-cards": "Archive All Cards in This List…", "list-archive-cards": "Archive All Cards in This List…",
"list-archive-cards-pop": "This will remove all the cards in this list from the board. To view archived cards and bring them back to the board, click “Menu” > “Archived Items”.", "list-archive-cards-pop": "This will remove all the cards in this list from the board. To view archived cards and bring them back to the board, click “Menu” > “Archived Items”.",
"log-in": "Log In", "log-in": "Log In",
@ -107,6 +108,7 @@
"members-title": "Add or remove members of the board from the card.", "members-title": "Add or remove members of the board from the card.",
"menu": "Menu", "menu": "Menu",
"modal-close-title": "Close this dialog window.", "modal-close-title": "Close this dialog window.",
"multi-selection": "Multi-Selection",
"my-boards": "My Boards", "my-boards": "My Boards",
"name": "Name", "name": "Name",
"name": "Name", "name": "Name",
@ -181,5 +183,6 @@
"changePermissionsPopup-title": "Change Permissions", "changePermissionsPopup-title": "Change Permissions",
"setLanguagePopup-title": "Change Language", "setLanguagePopup-title": "Change Language",
"cardAttachmentsPopup-title": "Attach From…", "cardAttachmentsPopup-title": "Attach From…",
"attachmentDeletePopup-title": "Delete Attachment?" "attachmentDeletePopup-title": "Delete Attachment?",
"disambiguateMultiLabelPopup-title": "Disambiguate Label Action"
} }