summaryrefslogtreecommitdiffstats
path: root/client/components/lists
diff options
context:
space:
mode:
Diffstat (limited to 'client/components/lists')
-rw-r--r--client/components/lists/body.jade50
-rw-r--r--client/components/lists/body.js73
-rw-r--r--client/components/lists/events.js16
-rw-r--r--client/components/lists/header.jade13
-rw-r--r--client/components/lists/header.js25
-rw-r--r--client/components/lists/main.jade5
-rw-r--r--client/components/lists/main.js81
-rw-r--r--client/components/lists/main.styl136
-rw-r--r--client/components/lists/menu.jade28
-rw-r--r--client/components/lists/menu.js46
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();
+ }
+});