diff options
Diffstat (limited to 'client/components/lists')
-rw-r--r-- | client/components/lists/body.jade | 50 | ||||
-rw-r--r-- | client/components/lists/body.js | 73 | ||||
-rw-r--r-- | client/components/lists/events.js | 16 | ||||
-rw-r--r-- | client/components/lists/header.jade | 13 | ||||
-rw-r--r-- | client/components/lists/header.js | 25 | ||||
-rw-r--r-- | client/components/lists/main.jade | 5 | ||||
-rw-r--r-- | client/components/lists/main.js | 81 | ||||
-rw-r--r-- | client/components/lists/main.styl | 136 | ||||
-rw-r--r-- | client/components/lists/menu.jade | 28 | ||||
-rw-r--r-- | client/components/lists/menu.js | 46 |
10 files changed, 473 insertions, 0 deletions
diff --git a/client/components/lists/body.jade b/client/components/lists/body.jade new file mode 100644 index 00000000..0e8efeeb --- /dev/null +++ b/client/components/lists/body.jade @@ -0,0 +1,50 @@ +template(name="listBody") + .minicards.clearfix.js-minicards + if cards.count + +inlinedForm(autoclose=false position="top") + +addCardForm + each cards + .minicard.card.js-minicard.js-member-droppable( + class="{{#if isSelected}}is-selected{{/if}}") + a.minicard-details.clearfix.show(href=absoluteUrl) + if cover + .minicard-cover.js-card-cover(style="background-image: url({{cover.url}});") + if labels + .minicard-labels + each labels + .minicard-label(class="card-label-{{color}}" title="{{name}}") + .minicard-title= title + if members + .minicard-members.js-minicard-members + each members + +userAvatar(userId=this size="small" cardId="{{../_id}}") + .badges + if comments.count + .badge(title="{{_ 'card-comments-title' comments.count }}") + span.badge-icon.icon-sm.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 + if attachments.count + .badge + span.badge-icon.icon-sm.fa.fa-paperclip + span.badge-text= attachments.count + if currentUser.isBoardMember + +inlinedForm(autoclose=false position="bottom") + +addCardForm + else + a.open-card-composer.js-open-inlined-form + i.fa.fa-plus + | {{_ 'add-card'}} + +template(name="addCardForm") + .minicard.js-composer + .minicard-labels.js-minicard-composer-labels + .minicard-details.clearfix + textarea.minicard-composer-textarea.js-card-title(autofocus) + = getCache + .minicard-members.js-minicard-composer-members + .add-controls.clearfix + button.primary.confirm(type="submit") {{_ 'add'}} + a.fa.fa-times.dark-hover.cancel.js-close-inlined-form diff --git a/client/components/lists/body.js b/client/components/lists/body.js new file mode 100644 index 00000000..fa6ec096 --- /dev/null +++ b/client/components/lists/body.js @@ -0,0 +1,73 @@ +BlazeComponent.extendComponent({ + template: function() { + return 'listBody'; + }, + + isSelected: function() { + return Session.equals('currentCard', this.currentData()._id); + }, + + addCard: function(evt) { + evt.preventDefault(); + var textarea = $(evt.currentTarget).find('textarea'); + var title = textarea.val(); + var position = this.currentData().position; + var sortIndex; + if (position === 'top') { + sortIndex = Utils.getSortIndex(null, this.find('.js-minicard:first')); + } else if (position === 'bottom') { + sortIndex = Utils.getSortIndex(this.find('.js-minicard:last'), null); + } + + // Clear the form in-memory cache + // var inputCacheKey = "addCard-" + this.listId; + // InputsCache.set(inputCacheKey, ''); + + // title trim if not empty then + if ($.trim(title)) { + 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); + }); + + // We keep the form opened, empty it, and scroll to it. + textarea.val('').focus(); + Utils.Scroll(this.find('.js-minicards')).top(1000, true); + } + }, + + events: function() { + return [{ + submit: this.addCard, + 'keydown form textarea': function(evt) { + // Pressing Enter should submit the card + if (evt.keyCode === 13) { + evt.preventDefault(); + $(evt.currentTarget).parents('form:first').submit(); + + // Pressing Tab should open the form of the next column, and Maj+Tab go + // in the reverse order + } else if (evt.keyCode === 9) { + evt.preventDefault(); + var isReverse = evt.shiftKey; + var list = $('#js-list-' + this.data()._id); + var nextList = list[isReverse ? 'prev' : 'next']('.js-list').get(0) || + $('.js-list:' + (isReverse ? 'last' : 'first')).get(0); + var nextListComponent = BlazeComponent.getComponentForElement(nextList); + + // XXX Get the real position + var position = 'bottom'; + nextListComponent.openForm({position: position}); + } + } + }]; + } +}).register('listBody'); diff --git a/client/components/lists/events.js b/client/components/lists/events.js new file mode 100644 index 00000000..f636de75 --- /dev/null +++ b/client/components/lists/events.js @@ -0,0 +1,16 @@ +Template.addlistForm.events({ + submit: function(event, t) { + event.preventDefault(); + var title = t.find('.list-name-input'); + if ($.trim(title.value)) { + Lists.insert({ + title: title.value, + boardId: Session.get('currentBoard'), + sort: $('.list').length + }); + + Utils.Scroll('.js-lists').left(270, true); + title.value = ''; + } + } +}); diff --git a/client/components/lists/header.jade b/client/components/lists/header.jade new file mode 100644 index 00000000..5196af5d --- /dev/null +++ b/client/components/lists/header.jade @@ -0,0 +1,13 @@ +template(name="listHeader") + .list-header.js-list-header + +inlinedForm + +editListTitleForm + else + h2.list-header-name.js-open-inlined-form= title + a.list-header-menu-icon.fa.fa-bars.js-open-list-menu + +template(name="editListTitleForm") + input.field.single-line(type="text" value="{{getCache title}}" autofocus) + .edit-controls.clearfix + input.primary.confirm(type="submit" value="{{_ 'save'}}") + a.fa.fa-times.js-close-inlined-form diff --git a/client/components/lists/header.js b/client/components/lists/header.js new file mode 100644 index 00000000..014cfd80 --- /dev/null +++ b/client/components/lists/header.js @@ -0,0 +1,25 @@ +BlazeComponent.extendComponent({ + template: function() { + return 'listHeader'; + }, + + editTitle: function(evt) { + evt.preventDefault(); + var form = this.componentChildren('inlinedForm')[0]; + var newTitle = form.getValue(); + if ($.trim(newTitle)) { + Lists.update(this.currentData()._id, { + $set: { + title: newTitle + } + }); + } + }, + + events: function() { + return [{ + 'click .js-open-list-menu': Popup.open('listAction'), + submit: this.editTitle + }]; + } +}).register('listHeader'); diff --git a/client/components/lists/main.jade b/client/components/lists/main.jade new file mode 100644 index 00000000..dd4bb49a --- /dev/null +++ b/client/components/lists/main.jade @@ -0,0 +1,5 @@ +template(name='list') + .list.js-list(id="js-list-{{_id}}") + .list-wrapper + +listHeader + +listBody diff --git a/client/components/lists/main.js b/client/components/lists/main.js new file mode 100644 index 00000000..3d458055 --- /dev/null +++ b/client/components/lists/main.js @@ -0,0 +1,81 @@ +ListComponent = BlazeComponent.extendComponent({ + template: function() { + return 'list'; + }, + + openForm: function(options) { + options = options || {}; + options.position = options.position || 'top'; + + var listComponent = this.componentChildren('listBody')[0]; + var forms = listComponent.componentChildren('inlinedForm'); + + if (options.position === 'top') { + forms[0].open(); + } else { + forms[forms.length - 1].open(); + } + }, + + // XXX The jQuery UI sortable plugin is far from ideal here. First we include + // all jQuery components but only use one. Second, it modifies the DOM itself, + // resulting in Blaze abandoning reactive update of the nodes that have been + // moved which result in bugs if multiple users use the board in real time. + // I tried sortable:sortable but that was not better. Should we “simply” write + // the drag&drop code ourselves? + onRendered: function() { + if (Meteor.user().isBoardMember()) { + var $cards = this.$('.js-minicards'); + $cards.sortable({ + connectWith: ".js-minicards", + tolerance: 'pointer', + appendTo: '.js-lists', + helper: "clone", + items: '.js-minicard:not(.placeholder, .hide, .js-composer)', + placeholder: 'minicard placeholder', + start: function (event, ui) { + $('.minicard.placeholder').height(ui.item.height()); + Popup.close(); + }, + stop: function(event, 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 + } + }); + } + }).disableSelection(); + + Utils.liveEvent('mouseover', function($el) { + $el.find('.js-member-droppable').droppable({ + hoverClass: "draggable-hover-card", + accept: '.js-member', + drop: function(event, ui) { + var memberId = Blaze.getData(ui.draggable.get(0)).userId; + var cardId = Blaze.getData(this)._id; + Cards.update(cardId, {$addToSet: {members: memberId}}); + } + }); + + $el.find('.js-member-droppable').droppable({ + hoverClass: "draggable-hover-card", + accept: '.js-label', + drop: function(event, ui) { + var labelId = Blaze.getData(ui.draggable.get(0))._id; + var cardId = Blaze.getData(this)._id; + Cards.update(cardId, {$addToSet: {labelIds: labelId}}); + } + }); + }); + } + } +}).register('list'); diff --git a/client/components/lists/main.styl b/client/components/lists/main.styl new file mode 100644 index 00000000..18484174 --- /dev/null +++ b/client/components/lists/main.styl @@ -0,0 +1,136 @@ +@import 'nib' + +.list + box-sizing: border-box + display: flex + flex-direction: column + flex: 0 0 270px + position: relative + // Even if this background color is the same as the body we can't leave it + // transparent, because that won't work during a list drag. + background: darken(white, 10%) + height: 100% + border-right: 1px solid darken(white, 17%) + border-left: 1px solid darken(white, 4%) + padding: 12px 7px 5px + overflow-y: auto + + &:first-child + margin-left: 5px + border-left: none + + &:last-child + margin-right: 5px + border-right: none + + &.editable + cursor: grab + + .list-wrapper + cursor: default + + &.add-list + &.fade + opacity: 0 + + .list-name-input + background: rgba(0, 0, 0, .05) + border-color: #aaa + box-shadow: inset 0 1px 8px rgba(0, 0, 0, .15) + display: block + margin: 0 + transition: margin 85ms ease-in, + background 85ms ease-in + width: 100% + + .edit-controls + height: 32px + transition: margin 85ms ease-in, + height 85ms ease-in + overflow: hidden + margin: 4px 0 0 + + input[type=submit] + margin-top: 0 + min-height: 30px + height: 30px + +.list-header + flex: 0 0 auto + padding: 10px 26px 4px 6px + position: relative + min-height: 20px + + .list-header-name + display: inline + font-size: 16px + line-height: 17px + margin: 0 + font-weight: bold + min-height: 9px + min-width: 30px + overflow: hidden + text-overflow: ellipsis + word-wrap: break-word + + .list-header-menu-icon + background-clip: content-box + background-origin: content-box + padding: 6px 8px + position: absolute + top: 3px + right: -5px + color: #a6a6a6 + + .list-header-num-cards + color: #8c8c8c + margin: 0 + +.minicards + // flex: 1 1 auto + overflow-y: auto + overflow-x: hidden + padding: 4px 4px 1px + z-index: 1 + height: 100% + + &::-webkit-scrollbar-button + display: block + height: 4px + +.open-card-composer + border-top-left-radius: 0 + border-top-right-radius: 0 + border-bottom-right-radius: 3px + border-bottom-left-radius: 3px + color: #8c8c8c + display: block + // flex: 0 0 auto + margin: 2px -3px -3px + padding: 7px 10px + position: relative + text-decoration: none + + &:hover + background: #c3c3c3 + color: #222 + text-decoration: underline + + + &::selection + background: transparent + +.list.placeholder + background-color: rgba(0, 0, 0, .2) + border-color: transparent + box-shadow: none + height: 100px + +.list.ui-sortable-helper + cursor: grabbing + box-shadow: -2px 2px 8px rgba(0, 0, 0, .3), 0 0 1px rgba(0, 0, 0, .5) + transform: rotate(4deg) + + +.list.ui-sortable-helper .list-header-menu-icon + display: none diff --git a/client/components/lists/menu.jade b/client/components/lists/menu.jade new file mode 100644 index 00000000..ff7820a4 --- /dev/null +++ b/client/components/lists/menu.jade @@ -0,0 +1,28 @@ +template(name="listActionPopup") + ul.pop-over-list + li: a.js-add-card {{_ 'add-card'}} + li: a.highlight-icon.js-list-subscribe {{_ 'subscribe'}} + if cards.count + hr + ul.pop-over-list + li: a.js-move-cards {{_ 'list-move-cards'}} + li: a.js-archive-cards {{_ 'list-archive-cards'}} + hr + ul.pop-over-list + li: a.js-close-list {{_ 'archive-list'}} + +template(name="listMoveCardsPopup") + +boardLists + +template(name="boardLists") + ul.pop-over-list + each currentBoard.lists + li + if($eq ../_id _id) + a.disabled {{title}} ({{_ 'current'}}) + else + a.js-select-list= title + +template(name="listArchiveCardsPopup") + p {{_ 'list-archive-cards-pop'}} + input.js-confirm.negate.full(type="submit" value="{{_ 'archive-all'}}") diff --git a/client/components/lists/menu.js b/client/components/lists/menu.js new file mode 100644 index 00000000..ef08cf76 --- /dev/null +++ b/client/components/lists/menu.js @@ -0,0 +1,46 @@ +Template.listActionPopup.events({ + 'click .js-add-card': function() { + // XXX We need a better API and architecture here. See + // https://github.com/peerlibrary/meteor-blaze-components/issues/19 + var listDom = document.getElementById('js-list-' + this._id); + var listComponent = Blaze.getView(listDom).templateInstance().get('component'); + listComponent.openForm(); + Popup.close(); + }, + 'click .js-list-subscribe': function() {}, + 'click .js-move-cards': Popup.open('listMoveCards'), + 'click .js-archive-cards': Popup.afterConfirm('listArchiveCards', function() { + Cards.find({listId: this._id}).forEach(function(card) { + Cards.update(card._id, { + $set: { + archived: true + } + }); + }); + Popup.close(); + }), + 'click .js-close-list': function(evt) { + evt.preventDefault(); + Lists.update(this._id, { + $set: { + archived: true + } + }); + Popup.close(); + } +}); + +Template.listMoveCardsPopup.events({ + 'click .js-select-list': function() { + var fromList = Template.parentData(2).data._id; + var toList = this._id; + Cards.find({listId: fromList}).forEach(function(card) { + Cards.update(card._id, { + $set: { + listId: toList + } + }); + }); + Popup.close(); + } +}); |