diff options
Diffstat (limited to 'client')
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("{{url}}");"> - <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 |