summaryrefslogtreecommitdiffstats
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to 'client')
-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
39 files changed, 797 insertions, 858 deletions
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