summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMaxime Quandalle <maxime@quandalle.com>2015-05-29 23:35:30 +0200
committerMaxime Quandalle <maxime@quandalle.com>2015-05-30 03:50:14 +0200
commit2c0030da62b9a1e59a55e3429fe514bbd51e1ee3 (patch)
treeb2834702806e59cb05ea02e2c377266eb17d6c8f
parent6457615e6ac6717d2175be9483388d4d70ea1c4a (diff)
downloadwekan-2c0030da62b9a1e59a55e3429fe514bbd51e1ee3.tar.gz
wekan-2c0030da62b9a1e59a55e3429fe514bbd51e1ee3.tar.bz2
wekan-2c0030da62b9a1e59a55e3429fe514bbd51e1ee3.zip
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.
-rw-r--r--.gitignore1
-rw-r--r--.jscsrc142
-rw-r--r--.jshintrc3
-rw-r--r--client/components/boards/boardBody.jade5
-rw-r--r--client/components/boards/boardBody.js19
-rw-r--r--client/components/boards/boardBody.styl5
-rw-r--r--client/components/boards/boardHeader.jade32
-rw-r--r--client/components/boards/boardHeader.js21
-rw-r--r--client/components/boards/colors.styl24
-rw-r--r--client/components/boards/router.js8
-rw-r--r--client/components/cards/details.styl27
-rw-r--r--client/components/cards/labels.styl5
-rw-r--r--client/components/cards/minicard.jade16
-rw-r--r--client/components/cards/minicard.js25
-rw-r--r--client/components/cards/minicard.styl119
-rw-r--r--client/components/cards/popups.jade5
-rw-r--r--client/components/forms/forms.styl68
-rw-r--r--client/components/forms/inlinedform.js4
-rw-r--r--client/components/lists/body.jade9
-rw-r--r--client/components/lists/body.js23
-rw-r--r--client/components/lists/main.js61
-rw-r--r--client/components/lists/main.styl5
-rw-r--r--client/components/lists/menu.jade1
-rw-r--r--client/components/lists/menu.js8
-rw-r--r--client/components/main/editor.js4
-rw-r--r--client/components/main/header.styl3
-rw-r--r--client/components/main/popup.styl239
-rw-r--r--client/components/sidebar/events.js17
-rw-r--r--client/components/sidebar/helpers.js14
-rw-r--r--client/components/sidebar/sidebar.jade39
-rw-r--r--client/components/sidebar/sidebar.js25
-rw-r--r--client/components/sidebar/sidebar.styl30
-rw-r--r--client/components/sidebar/sidebarFilters.jade57
-rw-r--r--client/components/sidebar/sidebarFilters.js94
-rw-r--r--client/components/sidebar/templates.html77
-rw-r--r--client/components/sidebar/templates.html.old307
-rw-r--r--client/config/router.js34
-rw-r--r--client/lib/filter.js11
-rw-r--r--client/lib/keyboard.js13
-rw-r--r--client/lib/multiSelection.js159
-rw-r--r--client/lib/popup.js4
-rw-r--r--client/styles/main.styl38
-rw-r--r--collections/cards.js6
-rw-r--r--collections/lists.js2
-rw-r--r--i18n/en.i18n.json7
45 files changed, 883 insertions, 933 deletions
diff --git a/.gitignore b/.gitignore
index f86ef4fc..986a8b46 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@
.meteor-spk
.tx/
*.sublime-workspace
+tmp/
diff --git a/.jscsrc b/.jscsrc
index f3fa8177..b795ce4a 100644
--- a/.jscsrc
+++ b/.jscsrc
@@ -1,73 +1,73 @@
{
- "disallowSpacesInNamedFunctionExpression": {
- "beforeOpeningRoundBrace": true
- },
- "disallowSpacesInFunctionExpression": {
- "beforeOpeningRoundBrace": true
- },
- "disallowSpacesInAnonymousFunctionExpression": {
- "beforeOpeningRoundBrace": true
- },
- "disallowSpacesInFunctionDeclaration": {
- "beforeOpeningRoundBrace": true
- },
- "disallowEmptyBlocks": true,
- "disallowSpacesInsideArrayBrackets": true,
- "disallowSpacesInsideParentheses": true,
- "disallowQuotedKeysInObjects": "allButReserved",
- "disallowSpaceAfterObjectKeys": true,
- "disallowSpaceAfterPrefixUnaryOperators": [
- "++",
- "--",
- "+",
- "-",
- "~"
- ],
- "disallowSpaceBeforePostfixUnaryOperators": true,
- "disallowSpaceBeforeBinaryOperators": [
- ","
- ],
- "disallowMixedSpacesAndTabs": true,
- "disallowTrailingWhitespace": true,
- "disallowTrailingComma": true,
- "disallowYodaConditions": true,
- "disallowKeywords": [ "with" ],
- "disallowMultipleLineBreaks": true,
- "disallowMultipleVarDecl": "exceptUndefined",
- "requireSpaceBeforeBlockStatements": true,
- "requireParenthesesAroundIIFE": true,
- "requireSpacesInConditionalExpression": true,
- "requireBlocksOnNewline": 1,
- "requireCommaBeforeLineBreak": true,
- "requireSpaceAfterPrefixUnaryOperators": [
- "!"
- ],
- "requireSpaceBeforeBinaryOperators": true,
- "requireSpaceAfterBinaryOperators": true,
- "requireCamelCaseOrUpperCaseIdentifiers": true,
- "requireLineFeedAtFileEnd": true,
- "requireCapitalizedConstructors": true,
- "requireDotNotation": true,
- "requireSpacesInForStatement": true,
- "requireSpaceBetweenArguments": true,
- "requireCurlyBraces": [
- "do"
- ],
- "requireSpaceAfterKeywords": [
- "if",
- "else",
- "for",
- "while",
- "do",
- "switch",
- "case",
- "return",
- "try",
- "catch",
- "typeof"
- ],
- "validateLineBreaks": "LF",
- "validateQuoteMarks": "'",
- "validateIndentation": 2,
- "maximumLineLength": 80
+ "disallowSpacesInNamedFunctionExpression": {
+ "beforeOpeningRoundBrace": true
+ },
+ "disallowSpacesInFunctionExpression": {
+ "beforeOpeningRoundBrace": true
+ },
+ "disallowSpacesInAnonymousFunctionExpression": {
+ "beforeOpeningRoundBrace": true
+ },
+ "disallowSpacesInFunctionDeclaration": {
+ "beforeOpeningRoundBrace": true
+ },
+ "disallowEmptyBlocks": true,
+ "disallowSpacesInsideArrayBrackets": true,
+ "disallowSpacesInsideParentheses": true,
+ "disallowQuotedKeysInObjects": "allButReserved",
+ "disallowSpaceAfterObjectKeys": true,
+ "disallowSpaceAfterPrefixUnaryOperators": [
+ "++",
+ "--",
+ "+",
+ "-",
+ "~"
+ ],
+ "disallowSpaceBeforePostfixUnaryOperators": true,
+ "disallowSpaceBeforeBinaryOperators": [
+ ","
+ ],
+ "disallowMixedSpacesAndTabs": true,
+ "disallowTrailingWhitespace": true,
+ "disallowTrailingComma": true,
+ "disallowYodaConditions": true,
+ "disallowKeywords": [ "with" ],
+ "disallowMultipleLineBreaks": true,
+ "disallowMultipleVarDecl": "exceptUndefined",
+ "requireSpaceBeforeBlockStatements": true,
+ "requireParenthesesAroundIIFE": true,
+ "requireSpacesInConditionalExpression": true,
+ "requireBlocksOnNewline": 1,
+ "requireCommaBeforeLineBreak": true,
+ "requireSpaceAfterPrefixUnaryOperators": [
+ "!"
+ ],
+ "requireSpaceBeforeBinaryOperators": true,
+ "requireSpaceAfterBinaryOperators": true,
+ "requireCamelCaseOrUpperCaseIdentifiers": true,
+ "requireLineFeedAtFileEnd": true,
+ "requireCapitalizedConstructors": true,
+ "requireDotNotation": true,
+ "requireSpacesInForStatement": true,
+ "requireSpaceBetweenArguments": true,
+ "requireCurlyBraces": [
+ "do"
+ ],
+ "requireSpaceAfterKeywords": [
+ "if",
+ "else",
+ "for",
+ "while",
+ "do",
+ "switch",
+ "case",
+ "return",
+ "try",
+ "catch",
+ "typeof"
+ ],
+ "validateLineBreaks": "LF",
+ "validateQuoteMarks": "'",
+ "validateIndentation": 2,
+ "maximumLineLength": 80
}
diff --git a/.jshintrc b/.jshintrc
index 9427f600..bf6b5ffe 100644
--- a/.jshintrc
+++ b/.jshintrc
@@ -69,9 +69,10 @@
// Our objects
"EscapeActions": true,
"Filter": true,
+ "Filter": true,
"Mixins": true,
+ "MultiSelection": true,
"Popup": true,
- "Filter": true,
"Sidebar": true,
"Utils": true,
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
- | (<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")
.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
+ | (<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
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 @@
+<!-- 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>
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 @@
-<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>
diff --git a/client/config/router.js b/client/config/router.js
index d4bc3c4f..ed9a069d 100644
--- a/client/config/router.js
+++ b/client/config/router.js
@@ -1,3 +1,6 @@
+// XXX Switch to Flow-Router?
+var previousRoute;
+
Router.configure({
loadingTemplate: 'spinner',
notFoundTemplate: 'notfound',
@@ -6,24 +9,43 @@ Router.configure({
onBeforeAction: function() {
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
// signup views.
- if (Meteor.userId() && options.redirectLoggedInUsers) {
+ if (loggedIn && options.redirectLoggedInUsers) {
return this.redirect('Boards');
}
// Authenticated
- if (! Meteor.userId() && options.authenticated) {
+ if (! loggedIn && options.authenticated) {
return this.redirect('atSignIn');
}
- // Reset default sessions
- Session.set('error', false);
-
Tracker.nonreactive(function() {
- EscapeActions.executeLowerThan(40);
+ if (! options.noEscapeActions &&
+ ! (previousRoute && previousRoute.options.noEscapeActions))
+ EscapeActions.executeAll();
});
+ previousRoute = this.route;
+
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 can’t 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');
+// }
+// });
diff --git a/client/lib/filter.js b/client/lib/filter.js
index d96fa89c..359b65d3 100644
--- a/client/lib/filter.js
+++ b/client/lib/filter.js
@@ -91,7 +91,7 @@ Filter = {
});
},
- getMongoSelector: function() {
+ _getMongoSelector: function() {
var self = this;
if (! self.isActive())
@@ -110,6 +110,14 @@ Filter = {
return {$or: [filterSelector, exceptionsSelector]};
},
+ mongoSelector: function(additionalSelector) {
+ var filterSelector = this._getMongoSelector();
+ if (_.isUndefined(additionalSelector))
+ return filterSelector;
+ else
+ return {$and: [filterSelector, additionalSelector]};
+ },
+
reset: function() {
var self = this;
_.forEach(self._fields, function(fieldName) {
@@ -123,6 +131,7 @@ Filter = {
if (this.isActive()) {
this._exceptions.push(_id);
this._exceptionsDep.changed();
+ Tracker.flush();
}
},
diff --git a/client/lib/keyboard.js b/client/lib/keyboard.js
index 0fbdbfd5..8b105c28 100644
--- a/client/lib/keyboard.js
+++ b/client/lib/keyboard.js
@@ -47,11 +47,16 @@ EscapeActions = {
'textcomplete',
'popup',
'inlinedForm',
+ 'multiselection-disable',
'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 })
var priority = this.hierarchy.indexOf(label);
if (priority === -1) {
@@ -87,6 +92,10 @@ EscapeActions = {
if (!! currentAction.condition())
currentAction.action();
}
+ },
+
+ executeAll: function() {
+ return this.executeLowerThan();
}
};
diff --git a/client/lib/multiSelection.js b/client/lib/multiSelection.js
new file mode 100644
index 00000000..53c16da0
--- /dev/null
+++ b/client/lib/multiSelection.js
@@ -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(); }
+);
diff --git a/client/lib/popup.js b/client/lib/popup.js
index 6298ba81..46c137e8 100644
--- a/client/lib/popup.js
+++ b/client/lib/popup.js
@@ -205,6 +205,6 @@ $(document).on('click', function(evt) {
// Press escape to close the popup.
var bindPopup = function(f) { return _.bind(f, Popup); };
EscapeActions.register('popup',
- bindPopup(Popup.isOpen),
- bindPopup(Popup.close)
+ bindPopup(Popup.close),
+ bindPopup(Popup.isOpen)
);
diff --git a/client/styles/main.styl b/client/styles/main.styl
index 521e1f56..4b78b9ec 100644
--- a/client/styles/main.styl
+++ b/client/styles/main.styl
@@ -318,44 +318,6 @@ dd
.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
- width: 80%
-
-input[type="submit"].attachment-add-link-submit
- float: left
- margin: 0 0 8px 4px
- padding: 6px 12px
- width: 18%
-
.card-detail-badge
background-color: #dbdbdb
border-radius: 3px
diff --git a/collections/cards.js b/collections/cards.js
index 538b6af4..374dcbc3 100644
--- a/collections/cards.js
+++ b/collections/cards.js
@@ -120,9 +120,15 @@ Cards.helpers({
});
return cardLabels;
},
+ hasLabel: function(labelId) {
+ return _.contains(this.labelIds, labelId);
+ },
user: function() {
return Users.findOne(this.userId);
},
+ isAssigned: function(memberId) {
+ return _.contains(this.members, memberId);
+ },
activities: function() {
return Activities.find({ type: 'card', cardId: this._id },
{ sort: { createdAt: -1 }});
diff --git a/collections/lists.js b/collections/lists.js
index 196477ec..1a30dbba 100644
--- a/collections/lists.js
+++ b/collections/lists.js
@@ -44,7 +44,7 @@ if (Meteor.isServer) {
Lists.helpers({
cards: function() {
- return Cards.find(_.extend(Filter.getMongoSelector(), {
+ return Cards.find(Filter.mongoSelector({
listId: this._id,
archived: false
}), { sort: ['sort'] });
diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json
index fcab8d20..60063ea8 100644
--- a/i18n/en.i18n.json
+++ b/i18n/en.i18n.json
@@ -74,7 +74,7 @@
"email-placeholder": "e.g., doc@frankenstein.com",
"filter": "Filter",
"filter-cards": "Filter Cards",
- "filter-clear": "Clear filter.",
+ "filter-clear": "Clear filter",
"filter-on": "Filter is on",
"filter-on-desc": "You are filtering cards on this board. Click here to edit filter.",
"fullname": "Full Name",
@@ -98,6 +98,7 @@
"leave-board": "Leave Board…",
"link-card": "Link to this card",
"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-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",
@@ -107,6 +108,7 @@
"members-title": "Add or remove members of the board from the card.",
"menu": "Menu",
"modal-close-title": "Close this dialog window.",
+ "multi-selection": "Multi-Selection",
"my-boards": "My Boards",
"name": "Name",
"name": "Name",
@@ -181,5 +183,6 @@
"changePermissionsPopup-title": "Change Permissions",
"setLanguagePopup-title": "Change Language",
"cardAttachmentsPopup-title": "Attach From…",
- "attachmentDeletePopup-title": "Delete Attachment?"
+ "attachmentDeletePopup-title": "Delete Attachment?",
+ "disambiguateMultiLabelPopup-title": "Disambiguate Label Action"
}