summaryrefslogtreecommitdiffstats
path: root/client/components/cards
diff options
context:
space:
mode:
authorMaxime Quandalle <maxime@quandalle.com>2015-05-12 19:20:58 +0200
committerMaxime Quandalle <maxime@quandalle.com>2015-05-12 19:33:50 +0200
commit2dbea30842ec63a68055245fe26633bb7913daf3 (patch)
treee9143893a3d3bf4ad34dd3a97d6f3466561c8756 /client/components/cards
downloadwekan-2dbea30842ec63a68055245fe26633bb7913daf3.tar.gz
wekan-2dbea30842ec63a68055245fe26633bb7913daf3.tar.bz2
wekan-2dbea30842ec63a68055245fe26633bb7913daf3.zip
Renaissance
_,,ad8888888888bba,_ ,ad88888I888888888888888ba, ,88888888I88888888888888888888a, ,d888888888I8888888888888888888888b, d88888PP"""" ""YY88888888888888888888b, ,d88"'__,,--------,,,,.;ZZZY8888888888888, ,8IIl'" ;;l"ZZZIII8888888888, ,I88l;' ;lZZZZZ888III8888888, ,II88Zl;. ;llZZZZZ888888I888888, ,II888Zl;. .;;;;;lllZZZ888888I8888b ,II8888Z;; `;;;;;''llZZ8888888I8888, II88888Z;' .;lZZZ8888888I888b II88888Z; _,aaa, .,aaaaa,__.l;llZZZ88888888I888 II88888IZZZZZZZZZ, .ZZZZZZZZZZZZZZ;llZZ88888888I888, II88888IZZ<'(@@>Z| |ZZZ<'(@@>ZZZZ;;llZZ888888888I88I ,II88888; `""" ;| |ZZ; `""" ;;llZ8888888888I888 II888888l `;; .;llZZ8888888888I888, ,II888888Z; ;;; .;;llZZZ8888888888I888I III888888Zl; .., `;; ,;;lllZZZ88888888888I888 II88888888Z;;...;(_ _) ,;;;llZZZZ88888888888I888, II88888888Zl;;;;;' `--'Z;. .,;;;;llZZZZ88888888888I888b ]I888888888Z;;;;' ";llllll;..;;;lllZZZZ88888888888I8888, II888888888Zl.;;"Y88bd888P";;,..;lllZZZZZ88888888888I8888I II8888888888Zl;.; `"PPP";;;,..;lllZZZZZZZ88888888888I88888 II888888888888Zl;;. `;;;l;;;;lllZZZZZZZZW88888888888I88888 `II8888888888888Zl;. ,;;lllZZZZZZZZWMZ88888888888I88888 II8888888888888888ZbaalllZZZZZZZZZWWMZZZ8888888888I888888, `II88888888888888888b"WWZZZZZWWWMMZZZZZZI888888888I888888b `II88888888888888888;ZZMMMMMMZZZZZZZZllI888888888I8888888 `II8888888888888888 `;lZZZZZZZZZZZlllll888888888I8888888, II8888888888888888, `;lllZZZZllllll;;.Y88888888I8888888b, ,II8888888888888888b .;;lllllll;;;.;..88888888I88888888b, II888888888888888PZI;. .`;;;.;;;..; ...88888888I8888888888, II888888888888PZ;;';;. ;. .;. .;. .. Y8888888I88888888888b, ,II888888888PZ;;' `8888888I8888888888888b, II888888888' 888888I8888888888888888 ,II888888888 ,888888I8888888888888888 ,d88888888888 d888888I8888888888ZZZZZZ ,ad888888888888I 8888888I8888ZZZZZZZZZZZZ 888888888888888' 888888IZZZZZZZZZZZZZZZZZ 8888888888P'8P' Y888ZZZZZZZZZZZZZZZZZZZZ 888888888, " ,ZZZZZZZZZZZZZZZZZZZZZZZ 8888888888, ,ZZZZZZZZZZZZZZZZZZZZZZZZZZ 888888888888a, _ ,ZZZZZZZZZZZZZZZZZZZZ88888888 888888888888888ba,_d' ,ZZZZZZZZZZZZZZZZZ8888888888888 8888888888888888888888bbbaaa,,,______,ZZZZZZZZZZZZZZZ88888888888888888 88888888888888888888888888888888888ZZZZZZZZZZZZZZZ88888888888888888888 8888888888888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888888 888888888888888888888888888888888ZZZZZZZZZZZZZZ88888888888888888888888 8888888888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888888888 88888888888888888888888888888ZZZZZZZZZZZZZZ888888888888888888888888888 8888888888888888888888888888ZZZZZZZZZZZZZZ88888888888888888 Normand 8 88888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888 Veilleux 8 8888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888888888888888
Diffstat (limited to 'client/components/cards')
-rw-r--r--client/components/cards/details.jade47
-rw-r--r--client/components/cards/details.js103
-rw-r--r--client/components/cards/details.styl161
-rw-r--r--client/components/cards/events.js285
-rw-r--r--client/components/cards/helpers.js48
-rw-r--r--client/components/cards/labels.styl183
-rw-r--r--client/components/cards/minicard.styl136
-rw-r--r--client/components/cards/popups.jade12
-rw-r--r--client/components/cards/router.js15
-rw-r--r--client/components/cards/templates.html336
10 files changed, 1326 insertions, 0 deletions
diff --git a/client/components/cards/details.jade b/client/components/cards/details.jade
new file mode 100644
index 00000000..0de59297
--- /dev/null
+++ b/client/components/cards/details.jade
@@ -0,0 +1,47 @@
+template(name="cardSidebar")
+ .card-sidebar.sidebar
+ .card-detail.sidebar-content.js-card-sidebar-content
+ if cover
+ .card-detail-cover(style="background-image: url({{ card.cover.url }})")
+ .card-detail-header(class="{{#if currentUser.isBoardMember}}editable{{/if}}")
+ a.js-close-card-detail
+ i.fa.fa-times
+ h2.card-detail-title.js-card-title= title
+ p.card-detail-list.js-move-card
+ | {{_ 'in-list'}}
+ a.card-detail-list-title(
+ class="{{#if currentUser.isBoardMember}}js-open-move-from-header is-editable{{/if}}")
+ = list.title
+ hr
+ //- if card.members
+ .card-detail-item.card-detail-item-members.clearfix.js-card-detail-members
+ h3.card-detail-item-header {{_ 'members'}}
+ .js-card-detail-members-list.clearfix
+ each members
+ +userAvatar(userId=this size="small" cardId=../_id)
+ a.card-detail-item-add-button.dark-hover.js-details-edit-members
+ i.fa.fa-plus
+ //- We should use "editable" to avoide repetiting ourselves
+ .clearfix
+ if currentUser.isBoardMember
+ h3 Description
+ +inlinedForm(classNames="js-card-description")
+ i.fa.fa-times.js-close-inlined-form
+ textarea(autofocus)= description
+ button(type="submit") {{_ 'edit'}}
+ else
+ .js-open-inlined-form
+ a {{_ 'edit'}}
+ +viewer
+ = description
+ else if description
+ h3 Description
+ +viewer
+ = description
+ hr
+ if attachments.count
+ +WindowAttachmentsModule(card=this)
+ +WindowActivityModule(card=this)
+
+template(name="moveCardPopup")
+ +boardLists
diff --git a/client/components/cards/details.js b/client/components/cards/details.js
new file mode 100644
index 00000000..a4fe89a3
--- /dev/null
+++ b/client/components/cards/details.js
@@ -0,0 +1,103 @@
+BlazeComponent.extendComponent({
+ template: function() {
+ return 'cardSidebar';
+ },
+
+ mixins: function() {
+ return [Mixins.InfiniteScrolling];
+ },
+
+ calculateNextPeak: function() {
+ var altitude = this.find('.js-card-sidebar-content').scrollHeight;
+ this.callFirstWith(this, 'setNextPeak', altitude);
+ },
+
+ reachNextPeak: function() {
+ var activitiesComponent = this.componentChildren('activities')[0];
+ activitiesComponent.loadNextPage();
+ },
+
+ events: function() {
+ return [{
+ 'click .js-move-card': Popup.open('moveCard'),
+ 'submit .js-card-description': function(evt) {
+ evt.preventDefault();
+ var cardId = Session.get('currentCard');
+ var form = this.componentChildren('inlinedForm')[0];
+ var newDescription = form.getValue();
+ Cards.update(cardId, {
+ $set: {
+ description: newDescription
+ }
+ });
+ form.close();
+ },
+ 'click .js-close-card-detail': function() {
+ Utils.goBoardId(Session.get('currentBoard'));
+ },
+ 'click .editable .js-card-title': function(event, t) {
+ var editable = t.$('.card-detail-title');
+
+ // add class editing and focus
+ $('.editing').removeClass('editing');
+ editable.addClass('editing');
+ editable.find('#title').focus();
+ },
+ 'click .js-edit-desc': function(event, t) {
+ var editable = t.$('.card-detail-item');
+
+ // editing remove based and add current editing.
+ $('.editing').removeClass('editing');
+ editable.addClass('editing');
+ editable.find('#desc').focus();
+
+ event.preventDefault();
+ },
+ 'click .js-cancel-edit': function(event, t) {
+ // remove editing hide.
+ $('.editing').removeClass('editing');
+ },
+ 'submit #WindowTitleEdit': function(event, t) {
+ var title = t.find('#title').value;
+ if ($.trim(title)) {
+ Cards.update(this.card._id, {
+ $set: {
+ title: title
+ }
+ }, function (err, res) {
+ if (!err) $('.editing').removeClass('editing');
+ });
+ }
+
+ event.preventDefault();
+ },
+ 'submit #WindowDescEdit': function(event, t) {
+ Cards.update(this.card._id, {
+ $set: {
+ description: t.find('#desc').value
+ }
+ }, function(err) {
+ if (!err) $('.editing').removeClass('editing');
+ });
+ event.preventDefault();
+ },
+ 'click .member': Popup.open('cardMember'),
+ 'click .js-details-edit-members': Popup.open('cardMembers'),
+ 'click .js-details-edit-labels': Popup.open('cardLabels')
+ }];
+ }
+}).register('cardSidebar');
+
+Template.moveCardPopup.events({
+ 'click .js-select-list': function() {
+ // XXX We should *not* get the currentCard from the global state, but
+ // instead from a “component” state.
+ var cardId = Session.get('currentCard');
+ var newListId = this._id;
+ Cards.update(cardId, {
+ $set: {
+ listId: newListId
+ }
+ });
+ }
+});
diff --git a/client/components/cards/details.styl b/client/components/cards/details.styl
new file mode 100644
index 00000000..faf15d79
--- /dev/null
+++ b/client/components/cards/details.styl
@@ -0,0 +1,161 @@
+@import 'nib'
+
+.card-detail.sidebar-content
+ width: 496px - 2 * 20px
+ top: -46px !important
+ z-index: 20 !important
+ // XXX Animate apparition
+
+ .card-detail-header
+ background: #F7F7F7
+ border-bottom: 1px solid darken(white, 10%)
+ position: absolute
+ min-height: 38px
+ top: 0
+ left: 0
+ right: 0
+ padding 7px 20px 0
+
+ i.fa
+ float: right
+ font-size: 1.3em
+ color: darken(white, 35%)
+ margin-top: 7px
+
+ .card-detail-title
+ font-weight: bold
+ font-size: 1.7em
+ margin: 3px 0 0
+ padding: 0
+
+ .card-detail-list
+ font-size: 0.85em
+ margin-bottom: 3px
+
+ a.card-detail-list-title
+ font-weight: bold
+
+ &.is-editable
+ display: inline-block
+ background: darken(white, 10%)
+ border-radius: 3px
+ padding: 0px 5px
+
+.new-comment
+ position: relative
+ margin: 0 0 20px 38px
+
+ .member
+ opacity: .7
+ position: absolute
+ top: 1px
+ left: -38px
+
+ .helper
+ bottom: 0
+ display: none
+ position: absolute
+ right: 9px
+
+ &.focus
+
+ .member
+ opacity: 1
+
+ .helper
+ display: inline-block
+
+ .new-comment-input
+ min-height: 108px
+ color: #4d4d4d
+ cursor: auto
+ overflow: hidden
+ word-wrap: break-word
+
+ .too-long
+ margin-top: 8px
+
+.new-comment-input
+ background-color: #fff
+ border: 0
+ box-shadow: 0 1px 2px rgba(0, 0, 0, .23)
+ color: #8c8c8c
+ height: 36px
+ margin: 4px 4px 6px 0
+ padding: 9px 11px
+ width: 100%
+
+ &:hover,
+ &:focus
+ background-color: #fff
+ box-shadow: 0 1px 3px rgba(0, 0, 0, .33)
+ border: 0
+ cursor: pointer
+
+ &:focus
+ cursor: auto
+
+.list-voters.compact .voter
+ position: relative
+ min-height: 36px
+
+ .member
+ left: 0
+ position: absolute
+ top: 0
+
+ .title
+ display: block
+ line-height: 30px
+ left: 0
+ overflow: hidden
+ padding-left: 38px
+ position: absolute
+ text-overflow: ellipsis
+ top: 0
+ white-space: nowrap
+ width: 230px
+
+.list-voters .title
+ display: none
+
+.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%
diff --git a/client/components/cards/events.js b/client/components/cards/events.js
new file mode 100644
index 00000000..9c270e8d
--- /dev/null
+++ b/client/components/cards/events.js
@@ -0,0 +1,285 @@
+// Template.cards.events({
+// // 'click .js-cancel': function(event, t) {
+// // var composer = t.$('.card-composer');
+
+// // // Keep the old value in memory to display it again next time
+// // var inputCacheKey = "addCard-" + this.listId;
+// // var oldValue = composer.find('.js-card-title').val();
+// // InputsCache.set(inputCacheKey, oldValue);
+
+// // // add composer hide class
+// // composer.addClass('hide');
+// // composer.find('.js-card-title').val('');
+
+// // // remove hide open link class
+// // $('.js-open-card-composer').removeClass('hide');
+// // },
+// 'submit': function(evt, tpl) {
+// evt.preventDefault();
+// var textarea = $(evt.currentTarget).find('textarea');
+// var title = textarea.val();
+// var lastCard = tpl.find('.js-minicard:last-child');
+// var sort;
+// if (lastCard === null) {
+// sort = 0;
+// } else {
+// sort = Blaze.getData(lastCard).sort + 1;
+// }
+// // debugger
+
+// // 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: Template.currentData().listId,
+// boardId: Template.currentData().board._id,
+// sort: sort
+// }, 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);
+// });
+
+// // empty and focus.
+// textarea.val('').focus();
+
+// // focus complete then scroll top
+// Utils.Scroll(tpl.find('.js-minicards')).top(1000, true);
+// }
+// }
+// });
+
+// Template.cards.events({
+// 'click .member': Popup.open('cardMember')
+// });
+
+Template.cardMemberPopup.events({
+ 'click .js-remove-member': function() {
+ Cards.update(this.cardId, {$pull: {members: this.userId}});
+ Popup.close();
+ }
+});
+
+Template.WindowActivityModule.events({
+ 'click .js-new-comment:not(.focus)': function(evt) {
+ var $this = $(evt.currentTarget);
+ $this.addClass('focus');
+ },
+ 'submit #CommentForm': function(evt, t) {
+ var text = t.$('.js-new-comment-input');
+ if ($.trim(text.val())) {
+ CardComments.insert({
+ boardId: this.card.boardId,
+ cardId: this.card._id,
+ text: text.val()
+ });
+ text.val('');
+ $('.focus').removeClass('focus');
+ }
+ evt.preventDefault();
+ }
+});
+
+Template.WindowSidebarModule.events({
+ 'click .js-change-card-members': Popup.open('cardMembers'),
+ 'click .js-edit-labels': Popup.open('cardLabels'),
+ 'click .js-archive-card': function(evt) {
+ // Update
+ Cards.update(this.card._id, {
+ $set: {
+ archived: true
+ }
+ });
+ evt.preventDefault();
+ },
+ 'click .js-unarchive-card': function(evt) {
+ Cards.update(this.card._id, {
+ $set: {
+ archived: false
+ }
+ });
+ evt.preventDefault();
+ },
+ 'click .js-delete-card': Popup.afterConfirm('cardDelete', function() {
+ Cards.remove(this.card._id);
+
+ // redirect board
+ Utils.goBoardId(this.card.board()._id);
+ Popup.close();
+ }),
+ 'click .js-more-menu': Popup.open('cardMore'),
+ 'click .js-attach': Popup.open('cardAttachments')
+});
+
+Template.WindowAttachmentsModule.events({
+ 'click .js-attach': Popup.open('cardAttachments'),
+ 'click .js-confirm-delete': Popup.afterConfirm('attachmentDelete',
+ function() {
+ Attachments.remove(this._id);
+ Popup.close();
+ }
+ ),
+ // If we let this event bubble, Iron-Router will handle it and empty the
+ // page content, see #101.
+ 'click .js-open-viewer, click .js-download': function(event) {
+ event.stopPropagation();
+ },
+ 'click .js-add-cover': function() {
+ Cards.update(this.cardId, { $set: { coverId: this._id } });
+ },
+ 'click .js-remove-cover': function() {
+ Cards.update(this.cardId, { $unset: { coverId: '' } });
+ }
+});
+
+Template.cardMembersPopup.events({
+ 'click .js-select-member': function(evt) {
+ var cardId = Template.parentData(2).data._id;
+ var memberId = this.userId;
+ var operation;
+ if (Cards.find({ _id: cardId, members: memberId}).count() === 0)
+ operation = '$addToSet';
+ else
+ operation = '$pull';
+
+ var query = {};
+ query[operation] = {
+ members: memberId
+ };
+ Cards.update(cardId, query);
+ evt.preventDefault();
+ }
+});
+
+Template.cardLabelsPopup.events({
+ 'click .js-select-label': function(evt) {
+ var cardId = Template.parentData(2).data._id;
+ var labelId = this._id;
+ var operation;
+ if (Cards.find({ _id: cardId, labelIds: labelId}).count() === 0)
+ operation = '$addToSet';
+ else
+ operation = '$pull';
+
+ var query = {};
+ query[operation] = {
+ labelIds: labelId
+ };
+ Cards.update(cardId, query);
+ evt.preventDefault();
+ },
+ 'click .js-edit-label': Popup.open('editLabel'),
+ 'click .js-add-label': Popup.open('createLabel')
+});
+
+Template.formLabel.events({
+ 'click .js-palette-color': function(evt) {
+ var $this = $(evt.currentTarget);
+
+ // hide selected ll colors
+ $('.js-palette-select').addClass('hide');
+
+ // show select color
+ $this.find('.js-palette-select').removeClass('hide');
+ }
+});
+
+Template.createLabelPopup.events({
+ // Create the new label
+ 'submit .create-label': function(evt, tpl) {
+ var name = tpl.$('#labelName').val().trim();
+ var boardId = Session.get('currentBoard');
+ var selectLabelDom = tpl.$('.js-palette-select:not(.hide)').get(0);
+ var selectLabel = Blaze.getData(selectLabelDom);
+ Boards.update(boardId, {
+ $push: {
+ labels: {
+ _id: Random.id(6),
+ name: name,
+ color: selectLabel.color
+ }
+ }
+ });
+ Popup.back();
+ evt.preventDefault();
+ }
+});
+
+Template.editLabelPopup.events({
+ 'click .js-delete-label': Popup.afterConfirm('deleteLabel', function() {
+ var boardId = Session.get('currentBoard');
+ Boards.update(boardId, {
+ $pull: {
+ labels: {
+ _id: this._id
+ }
+ }
+ });
+ Popup.back(2);
+ }),
+ 'submit .edit-label': function(evt, tpl) {
+ var name = tpl.$('#labelName').val().trim();
+ var boardId = Session.get('currentBoard');
+ var getLabel = Utils.getLabelIndex(boardId, this._id);
+ var selectLabelDom = tpl.$('.js-palette-select:not(.hide)').get(0);
+ var selectLabel = Blaze.getData(selectLabelDom);
+ var $set = {};
+
+ // set label index
+ $set[getLabel.key('name')] = name;
+
+ // set color
+ $set[getLabel.key('color')] = selectLabel.color;
+
+ // update
+ Boards.update(boardId, { $set: $set });
+
+ // return to the previous popup view trigger
+ Popup.back();
+
+ evt.preventDefault();
+ },
+ 'click .js-select-label': function() {
+ Cards.remove(this.cardId);
+
+ // redirect board
+ Utils.goBoardId(this.boardId);
+ }
+});
+
+Template.cardMorePopup.events({
+ 'click .js-delete': Popup.afterConfirm('cardDelete', function() {
+ Cards.remove(this.card._id);
+
+ // redirect board
+ Utils.goBoardId(this.card.board()._id);
+ })
+});
+
+Template.cardAttachmentsPopup.events({
+ 'change .js-attach-file': function(evt) {
+ var card = this.card;
+ FS.Utility.eachFile(evt, function(f) {
+ var file = new FS.File(f);
+
+ // set Ids
+ file.boardId = card.boardId;
+ file.cardId = card._id;
+
+ // upload file
+ Attachments.insert(file);
+
+ Popup.close();
+ });
+ },
+ 'click .js-computer-upload': function(evt, t) {
+ t.find('.js-attach-file').click();
+ evt.preventDefault();
+ }
+});
diff --git a/client/components/cards/helpers.js b/client/components/cards/helpers.js
new file mode 100644
index 00000000..708b1b56
--- /dev/null
+++ b/client/components/cards/helpers.js
@@ -0,0 +1,48 @@
+Template.cardMembersPopup.helpers({
+ isCardMember: function() {
+ var cardId = Template.parentData()._id;
+ var cardMembers = Cards.findOne(cardId).members || [];
+ return _.contains(cardMembers, this.userId);
+ },
+ user: function() {
+ return Users.findOne(this.userId);
+ }
+});
+
+Template.cardLabelsPopup.helpers({
+ isLabelSelected: function(cardId) {
+ return _.contains(Cards.findOne(cardId).labelIds, this._id);
+ }
+});
+
+var labelColors;
+Meteor.startup(function() {
+ labelColors = Boards.simpleSchema()._schema['labels.$.color'].allowedValues;
+});
+
+Template.createLabelPopup.helpers({
+ // This is the default color for a new label. We search the first color that
+ // is not already used in the board (although it's not a problem if two
+ // labels have the same color).
+ defaultColor: function() {
+ var labels = this.labels || this.card.board().labels;
+ var usedColors = _.pluck(labels, 'color');
+ var availableColors = _.difference(labelColors, usedColors);
+ return availableColors.length > 1 ? availableColors[0] : 'green';
+ }
+});
+
+Template.formLabel.helpers({
+ labels: function() {
+ return _.map(labelColors, function(color) {
+ return { color: color, name: '' };
+ });
+ }
+});
+
+Blaze.registerHelper('currentCard', function() {
+ var cardId = Session.get('currentCard');
+ if (cardId) {
+ return Cards.findOne(cardId);
+ }
+});
diff --git a/client/components/cards/labels.styl b/client/components/cards/labels.styl
new file mode 100644
index 00000000..27058b21
--- /dev/null
+++ b/client/components/cards/labels.styl
@@ -0,0 +1,183 @@
+@import 'nib'
+
+// XXX Use .board-widget-labels as a flexbox container
+.card-label
+ background-color: #b3b3b3
+ border-radius: 4px
+ color: white
+ display: inline-block
+ font-weight: 700
+ font-size: 13px
+ margin-right: 4px
+ padding: 3px 8px
+ position:relative
+ max-width: 100%
+ min-width: 8px
+ overflow: ellipsis
+ height: 18px
+
+ &:hover
+ color: white
+
+.card-label-green
+ background-color: #3cb500
+
+.card-label-yellow
+ background-color: #fad900
+
+.card-label-orange
+ background-color: #ff9f19
+
+.card-label-red
+ background-color: #eb4646
+
+.card-label-purple
+ background-color: #a632db
+
+.card-label-blue
+ background-color: #0079bf
+
+.card-label-pink
+ background-color: #ff78cb
+
+.card-label-sky
+ background-color: #00c2e0
+
+.card-label-black
+ background-color: #4d4d4d
+
+.card-label-lime
+ background-color: #51e898
+
+.edit-label,
+.create-label
+ .card-label
+ float: left
+ height: 25px
+ margin: 0px 3% 7px 0px
+ width: 10.5%
+ cursor: pointer
+
+.edit-labels
+ input[type="text"]
+ margin: 4px 0 6px 38px
+ width: 243px
+
+ .card-label
+ height: 30px
+ left: 0
+ padding: 1px 5px
+ position: absolute
+ top: 0
+ width: 24px
+
+ .labels-static .card-label
+ line-height: 30px
+ margin-bottom: 4px
+ position: relative
+ top: auto
+ left: 0
+ width: 260px
+
+.minicard-labels
+ position: relative
+ z-index: 30
+ top: -6px
+
+ .card-label
+ border-radius: 0
+ float: left
+ height: 4px
+ margin-bottom: 1px
+ padding: 0
+ width: 40px
+ line-height: 100px
+
+.card-detail-item-labels .card-label
+ border-radius: 3px
+ display: block
+ float: left
+ height: 20px
+ line-height: 20px
+ margin: 0 4px 4px 0
+ min-width: 30px
+ padding: 5px 10px
+ width: auto
+
+.editable-labels .card-label:hover
+ cursor: pointer
+ opacity: .75
+
+.edit-labels-pop-over
+ margin-bottom: 8px
+
+.edit-labels-pop-over .shortcut
+ display: inline-block
+
+.card-label-selectable
+ border-radius: 3px
+ cursor: pointer
+ margin: 0 50px 4px 0
+ min-height: 18px
+ padding: 8px
+ position: relative
+ transition: margin-right .1s
+
+ .card-label-selectable-icon
+ position: absolute
+ top: 8px
+ right: -20px
+
+ &.active:hover,
+ &.active,
+ &.active.selected:hover,
+ &.active.selected
+ margin-right: 38px
+ padding-right: 32px
+
+ .card-label-selectable-icon
+ right: 6px
+
+ &.active:hover:hover,
+ &.active:hover,
+ &.active.selected:hover:hover,
+ &.active.selected:hover
+ margin-right: 38px
+
+ &.selected,
+ &:hover
+ margin-right: 38px
+ opacity: .8
+
+.active .card-label-selectable
+ &,
+ &:hover
+ margin-right: 0
+
+ .card-label-selectable-icon
+ right: 8px
+
+.card-label-edit-button
+ border-radius: 3px
+ float: right
+ padding: 8px
+
+ &:hover
+ background: #dbdbdb
+
+.card-label-color-select-icon
+ left: 14px
+ position: absolute
+ top: 9px
+
+.phenom .card-label
+ display: inline-block
+ font-size: 12px
+ height: 14px
+ line-height: 13px
+ padding: 0 4px
+ min-width: 16px
+ overflow: ellipsis
+
+.board-widget .phenom .card-label
+ max-width: 130px
diff --git a/client/components/cards/minicard.styl b/client/components/cards/minicard.styl
new file mode 100644
index 00000000..a78cd46f
--- /dev/null
+++ b/client/components/cards/minicard.styl
@@ -0,0 +1,136 @@
+.minicard
+ background-color: #fff
+ box-shadow: 0 1px 2px rgba(0,0,0,.2)
+ border-radius: 2px
+ cursor: pointer
+ margin-bottom: 9px
+ max-width: 300px
+ min-height: 20px
+ position: relative
+ z-index: 0
+ overflow: hidden
+
+ 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
+
+ .minicard-cover
+ background-position: center
+ background-repeat: no-repeat
+ background-size: cover
+ height: 145px
+ user-select: none
+ margin: -6px -8px 6px -8px
+ border-radius: top 2px
+
+ &.no-preview-size
+ background-size: auto
+ background-position: center
+
+ .minicard-details
+ padding: 6px 8px 2px
+ position: relative
+ z-index: 10
+
+
+ &.is-selected
+ .minicard-details
+ padding-bottom: 0
+
+ a.minicard-details
+ text-decoration:none
+
+ .minicard-details-overlay
+ background: transparent
+ bottom: 0
+ left: 0
+ position: absolute
+ right: 0
+ top: 0
+
+ .minicard-dropzone
+ display: none
+
+ .minicard.drophover .minicard-dropzone
+ background: rgba(255, 255, 255, .8)
+ // border-radius: 3px
+ // bottom: 0
+ // display: block
+ // font-weight: 700
+ // line-height: 100%
+ // left: 0
+ // margin: 0
+ // opacity: 1
+ // padding: 0
+ // position: absolute
+ // right: 0
+ // text-align: center
+ // top: 0
+ // z-index: 40
+
+ .minicard-title
+ display: block
+ font-weight: 400
+ margin: 0 0 4px
+ overflow: hidden
+ text-decoration: none
+ word-wrap: break-word
+
+ &::selection
+ background: transparent
+
+ .minicard-labels
+ padding-top: 3px
+ margin-top: 4px
+ float: right
+
+ .minicard-label
+ float: right
+ width: 8px
+ height: @width
+ border-radius: 2px
+ margin-left: 4px
+
+ .minicard-members
+ float: right
+ margin: 2px -8px -2px 0
+
+ .member
+ float: right
+ border-radius: 50%
+ height: 28px
+ width: @height
+
+ + .badges
+ margin-top: 10px
+
+ .minicard-members:empty
+ display: none
+
+.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
diff --git a/client/components/cards/popups.jade b/client/components/cards/popups.jade
new file mode 100644
index 00000000..0b5aa4c0
--- /dev/null
+++ b/client/components/cards/popups.jade
@@ -0,0 +1,12 @@
+template(name="cardMembersPopup")
+ //- input.js-search-mem(autofocus placeholder="Search members…" type="text")
+ ul.pop-over-member-list.checkable.js-mem-list
+ each board.members
+ li.item.js-member-item(class="{{#if isCardMember}}active{{/if}}")
+ a.name.js-select-member(href="#")
+ +userAvatar(user=user size="small")
+ span.full-name
+ = user.profile.name
+ | (<span class="username">{{ user.username }}</span>)
+ if isCardMember
+ i.fa.fa-check
diff --git a/client/components/cards/router.js b/client/components/cards/router.js
new file mode 100644
index 00000000..48bb9a95
--- /dev/null
+++ b/client/components/cards/router.js
@@ -0,0 +1,15 @@
+Router.route('/boards/:boardId/:slug/:cardId', {
+ name: 'Card',
+ template: 'board',
+ waitOn: function() {
+ var params = this.params;
+ // XXX We probably shouldn't rely on Session
+ Session.set('currentBoard', params.boardId);
+ Session.set('currentCard', params.cardId);
+
+ return BoardSubsManager.subscribe('board', params.boardId, params.slug);
+ },
+ data: function() {
+ return Boards.findOne(this.params.boardId);
+ }
+});
diff --git a/client/components/cards/templates.html b/client/components/cards/templates.html
new file mode 100644
index 00000000..4c65e429
--- /dev/null
+++ b/client/components/cards/templates.html
@@ -0,0 +1,336 @@
+<template name="cardModal">
+ {{ > modal template='cardDetailWindow' card=this board=this.board }}
+</template>
+
+<template name="cardMemberPopup">
+ <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">
+ <li><a class="js-remove-member">{{_ 'remove-member-from-card'}}</a></li>
+ </ul>
+ {{/ if }}
+ </div>
+</template>
+
+<template name="cardMorePopup">
+ <p class="quiet bottom">
+ <span class="clearfix">
+ <span>{{_ 'link-card'}}</span>
+ <span class="icon-sm fa {{#if card.board.isPublic}}fa-globe{{else}}fa-lock{{/if}}"></span>
+ <input class="js-url js-autoselect inline-input" type="text" readonly="readonly" value="{{ card.rootUrl }}">
+ </span>
+ {{_ 'added'}} <span class="date" title="{{ card.createdAt }}">{{ moment card.createdAt 'LLL' }}</span> -
+ <a class="js-delete" href="#" title="{{_ 'card-delete-notice'}}">{{_ 'delete'}}</a>
+ </p>
+</template>
+
+<template name="cardLabelsPopup">
+ <div>
+ {{! <input id="labelSearch" name="search" class="js-autofocus js-label-search" placeholder="Search labels…" value="" type="text"> }}
+ <ul class="edit-labels-pop-over js-labels-list">
+ {{# each card.board.labels }}
+ <li>
+ <a href="#" class="card-label-edit-button icon-sm fa fa-pencil js-edit-label"></a>
+ <span class="card-label card-label-selectable card-label-{{color}} js-select-label {{# if isLabelSelected ../card._id }}active{{/ if }}">
+ {{name}}
+ {{# if currentUser.isBoardAdmin }}
+ <span class="card-label-selectable-icon icon-sm fa fa-check light"></span>
+ {{/ if }}
+ </span>
+ </li>
+ {{/ each}}
+ </ul>
+ <a class="quiet-button full js-add-label">{{_ 'label-create'}}</a>
+ </div>
+</template>
+
+<template name="cardAttachmentsPopup">
+ <div>
+ <ul class="pop-over-list">
+ <li>
+ <input type="file" name="file" class="js-attach-file hide" multiple>
+ <a class="js-computer-upload" href="#">
+ {{_ 'computer'}}
+ </a>
+ </li>
+ </ul>
+ </div>
+</template>
+
+<template name="formLabel">
+ <div class="colors clearfix">
+ <label for="labelName">{{_ 'name'}}</label>
+ <input id="labelName" type="text" name="name" class="js-label-name" value='{{ name }}' autofocus>
+ <label>{{_ "select-color"}}</label>
+ {{# each labels }}
+ <span class="card-label card-label--selectable card-label-{{ color }} palette-color js-palette-color">
+ <span class="card-label-color-select-icon icon-sm fa fa-check light js-palette-select {{#if $neq color ../color}}hide{{/if}}"></span>
+ </span>
+ {{/each}}
+ </div>
+</template>
+
+<template name="createLabelPopup">
+ <form class="create-label">
+ {{#with color=defaultColor}}
+ {{> formLabel}}
+ {{/with}}
+ <input type="submit" class="primary wide left" value="{{_ 'create'}}">
+ </form>
+</template>
+
+<template name="editLabelPopup">
+ <form class="edit-label">
+ {{> formLabel}}
+ <input type="submit" class="primary wide left" value="{{_ 'save'}}">
+ <span class="right">
+ <input type="submit" value="{{_ 'delete'}}" class="negate js-delete-label">
+ </span>
+ </form>
+</template>
+
+<template name="deleteLabelPopup">
+ <p>{{_ "label-delete-pop"}}</p>
+ <input type="submit" class="js-confirm negate full" value="{{_ 'delete'}}">
+</template>
+
+<template name="cardDeletePopup">
+ <p>{{_ "card-delete-pop"}}</p>
+ <input type="submit" class="js-confirm negate full" value="{{_ 'delete'}}">
+</template>
+
+<template name="attachmentDeletePopup">
+ <p>{{_ "attachment-delete-pop"}}</p>
+ <input type="submit" class="js-confirm negate full" value="{{_ 'delete'}}">
+</template>
+
+<template name="cardDetailSidebarOld">
+ <div class="card-detail-window clearfix">
+ {{# if card.cover }}
+ <div class="window-cover js-card-cover-box js-open-card-cover-in-viewer has-cover" style="background-image: url({{ card.cover.url }}); background-color: rgb(119, 119, 119); background-size: contain;">
+ </div>
+ {{ /if }}
+ {{ #if card.archived }}
+ <div class="window-archive-banner js-archive-banner">
+ <span class="icon-lg fa fa-archive window-archive-banner-icon"></span>
+ <p class="window-archive-banner-text">{{_ "card-archived"}}</p>
+ </div>
+ {{ /if }}
+ <div class="window-header clearfix">
+ <span class="window-header-icon icon-lg fa fa-calendar-o"></span>
+ <div class="window-title card-detail-title non-empty inline {{# if currentUser.isBoardMember }}editable{{/ if }}">
+ <h2 class="window-title-text current hide-on-edit js-card-title">{{ card.title }}</h2>
+ <div class="edit edit-heavy">
+ <form id="WindowTitleEdit">
+ <textarea type="text" class="field single-line" id="title">{{ card.title }}</textarea>
+ <div class="edit-controls clearfix">
+ <input type="submit" class="primary confirm js-title-save-edit" value="{{_ 'save'}}">
+ <a href="#" class="icon-lg fa fa-times dark-hover cancel js-cancel-edit"></a>
+ </div>
+ </form>
+ </div>
+ <div class="quiet hide-on-edit window-header-inline-content js-current-list">
+ <p class="inline-block bottom">
+ {{_ 'in-list'}}
+ <a href="#" class="{{# if currentUser.isBoardMember }}js-open-move-from-header{{else}}disabled{{/ if }}"><strong>{{ card.list.title }}</strong></a>
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="window-main-col clearfix">
+ <div class="card-detail-data gutter clearfix">
+ <div class="card-detail-item card-detail-item-block clear clearfix editable">
+ {{# if card.members }}
+ <div class="card-detail-item card-detail-item-members clearfix js-card-detail-members">
+ <h3 class="card-detail-item-header">{{_ 'members'}}</h3>
+ <div class="js-card-detail-members-list clearfix">
+ {{# each card.members }}
+ {{> userAvatar userId=this size="small" cardId=../card._id }}
+ {{/ each }}
+ <a class="card-detail-item-add-button dark-hover js-details-edit-members">
+ <span class="icon-sm fa fa-plus"></span>
+ </a>
+ </div>
+ </div>
+ {{/ if }}
+ {{# if card.labels }}
+ <div class="card-detail-item card-detail-item-labels clearfix js-card-detail-labels">
+ <h3 class="card-detail-item-header">{{_ 'labels'}}</h3>
+ <div class="js-card-detail-labels-list clearfix editable-labels js-edit-label">
+ {{# each card.labels }}
+ <span class="card-label card-label-{{color}}" title="{{name}}">{{ name }}</span>
+ {{/ each }}
+ <a class="card-detail-item-add-button dark-hover js-details-edit-labels">
+ <span class="icon-sm fa fa-plus"></span>
+ </a>
+ </div>
+ </div>
+ {{/ if }}
+ <div class="card-detail-item card-detail-item-block clear clearfix editable" attr="desc">
+ {{# if card.description }}
+ <h3 class="card-detail-item-header js-show-with-desc">{{_ 'description'}}</h3>
+ {{# if currentUser.isBoardMember }}
+ <a href="#" class="card-detail-item-header-edit hide-on-edit js-show-with-desc js-edit-desc">{{_ 'edit'}}</a>
+ {{/ if }}
+ <div class="current markeddown hide-on-edit js-card-desc js-show-with-desc">
+ {{#viewer}}{{ card.description }}{{/viewer}}
+ </div>
+ {{ else }}
+ {{# if currentUser.isBoardMember }}
+ <p class="bottom">
+ <a href="#" class="hide-on-edit quiet-button w-img js-edit-desc js-hide-with-desc">
+ <span class="icon-sm fa fa-align-left"></span>
+ {{_ 'edit-description'}}
+ </a>
+ </p>
+ {{/ if }}
+ {{/ if }}
+ <div class="card-detail-edit edit">
+ <form id="WindowDescEdit">
+ {{#editor class="field single-line2" id="desc"}}{{ card.description }}{{/editor}}
+ <div class="edit-controls clearfix">
+ <input type="submit" class="primary confirm js-title-save-edit" value="{{_ 'save'}}">
+ <a href="#" class="icon-lg fa fa-times dark-hover cancel js-cancel-edit"></a>
+ </div>
+ </form>
+ </div>
+ </div>
+ </div>
+ </div>
+ {{# if card.attachments.count }}
+ {{ > WindowAttachmentsModule card=card }}
+ {{/ if}}
+ {{ > WindowActivityModule card=card }}
+ </div>
+ {{# if currentUser.isBoardMember }}
+ {{ > WindowSidebarModule card=card }}
+ {{/if}}
+ </div>
+</template>
+
+<template name="WindowActivityModule">
+ <div class="card-detailwindow-module">
+ <div class="window-module-title window-module-title-no-divider">
+ <span class="window-module-title-icon icon-lg fa fa-comments-o"></span>
+ <h3>{{ _ 'activity'}}</h3>
+ </div>
+ {{# if currentUser.isBoardMember }}
+ <div class="new-comment js-new-comment">
+ {{> userAvatar user=currentUser size="small" class="member-no-menu" }}
+ <form id="CommentForm">
+ {{#editor class="new-comment-input js-new-comment-input"}}{{/editor}}
+ <div class="add-controls clearfix">
+ <input type="submit" class="primary confirm clear js-add-comment" value="{{_ 'comment'}}" tabindex="2">
+ </div>
+ </form>
+ </div>
+ {{/ if }}
+ {{ > activities mode="card" }}
+ </div>
+</template>
+
+<template name="WindowAttachmentsModule">
+ <div class="window-module js-attachments-section clearfix">
+ <div class="window-module-title window-module-title-no-divider">
+ <span class="window-module-title-icon icon-lg fa fa-paperclip"></span>
+ <h3 class="inline-block">{{_ 'attachments'}}</h3>
+ </div>
+ <div class="gutter">
+ <div class="clearfix js-attachment-list">
+ {{# each card.attachments }}
+ <div class="attachment-thumbnail">
+ {{# if isUploaded }}
+ <a href="{{ url download=true }}" class="attachment-thumbnail-preview js-open-viewer attachment-thumbnail-preview-is-cover">
+ {{# if isImage }}
+ <img src="{{ url }}">
+ {{ else }}
+ <span class="attachment-thumbnail-preview-ext">{{ extension }}</span>
+ {{ /if }}
+ </a>
+ <p class="attachment-thumbnail-details js-open-viewer">
+ <a href="" class="attachment-thumbnail-details-title js-attachment-thumbnail-details">
+ {{ name }}
+ <span class="block quiet">
+ {{_ 'added'}} <span class="date">{{ moment uploadedAt }}</span>
+ </span>
+ </a>
+ <span class="quiet attachment-thumbnail-details-options">
+ <a href="{{ url download=true }}" class="attachment-thumbnail-details-options-item dark-hover js-download">
+ <span class="icon-sm fa fa-download"></span>
+ <span class="attachment-thumbnail-details-options-item-text">{{_ 'download'}}</span>
+ </a>
+ {{# if isImage }}
+ <a class="attachment-thumbnail-details-options-item dark-hover {{#if $eq ../card.coverId _id}}js-remove-cover{{else}}js-add-cover{{/if}}">
+ <span class="icon-sm fa fa-thumb-tack"></span>
+ <span class="attachment-thumbnail-details-options-item-text">{{#if $eq ../card.coverId _id}}{{_ 'remove-cover'}}{{else}}{{_ 'add-cover'}}{{/if}}</span>
+ </a>
+ {{/if}}
+ <a href="#" class="attachment-thumbnail-details-options-item attachment-thumbnail-details-options-item-delete dark-hover js-confirm-delete">
+ <span class="icon-sm fa fa-close"></span>
+ <span class="attachment-thumbnail-details-options-item-text">{{_ 'delete'}}</span>
+ </a>
+ </span>
+ </p>
+ {{ else }}
+ +spinner
+ {{/ if }}
+ </div>
+ {{/each}}
+ </div>
+ <p>
+ <a href="#" class="quiet-button js-attach">{{_ 'add-attachment' }}</a>
+ </p>
+ </div>
+ </div>
+</template>
+
+<template name="WindowSidebarModule">
+ <div class="window-sidebar" style="position: relative;">
+ <div class="window-module clearfix">
+ <h3>{{_ 'add'}}</h3>
+ <div class="clearfix">
+ <a href="#" class="button-link js-change-card-members" title="{{_ 'members-title'}}">
+ <span class="icon-sm fa fa-user"></span> {{_ 'members'}}
+ </a>
+ <a href="#" class="button-link js-edit-labels" title="{{_ 'labels-title'}}">
+ <span class="icon-sm fa fa-tags"></span> {{_ 'labels'}}
+ </a>
+ <a href="#" class="button-link js-attach" title="{{_ 'attachment-title'}}">
+ <span class="icon-sm fa fa-paperclip"></span> {{_ 'attachment'}}
+ </a>
+ </div>
+ </div>
+ <div class="window-module other-actions clearfix">
+ <h3>{{_ 'actions'}}</h3>
+ <div class="clearfix">
+ <hr>
+ {{ #if card.archived }}
+ <a href="#" class="button-link js-unarchive-card" title="{{_ 'send-to-board-title'}}">
+ <span class="icon-sm fa fa-recycle"></span> {{_ 'send-to-board'}}
+ </a>
+ <a href="#" class="button-link negate js-delete-card" title="{{_ 'delete-title'}}">
+ <span class="icon-sm fa fa-trash-o"></span> {{_ 'delete'}}
+ </a>
+ {{ else }}
+ <a href="#" class="button-link js-archive-card" title="{{_ 'archive-title'}}">
+ <span class="icon-sm fa fa-archive"></span> {{_ 'archive'}}
+ </a>
+ {{ /if }}
+ </div>
+ </div>
+ <div class="window-module clearfix">
+ <p class="quiet bottom">
+ <a href="#" class="quiet-button js-more-menu" title="{{_ 'share-and-more-title'}}">{{_ 'share-and-more'}}</a>
+ </p>
+ </div>
+ </div>
+</template>