Work on the card activities and comments

This commit also introduces a new CSSEvents object that is used to
abstract vendor specifics events related to CSS transitions and
animations.

Fixes #183.
Fixes #179.
This commit is contained in:
Maxime Quandalle 2015-06-12 13:59:39 +02:00
parent 216887490e
commit c894567987
31 changed files with 590 additions and 691 deletions

View file

@ -71,6 +71,7 @@
"AccountsTemplates": true,
// Our objects
"CSSEvents": true,
"EscapeActions": true,
"Filter": true,
"Filter": true,

View file

@ -1,8 +1,113 @@
template(name="activities")
.js-sidebar-activities
.activities.js-sidebar-activities
//- We should use Template.dynamic here but there is a bug with
//- blaze-components: https://github.com/peerlibrary/meteor-blaze-components/issues/30
if $eq mode "board"
+boardActivities
else
+cardActivities
template(name="boardActivities")
each currentBoard.activities
.activity
+userAvatar(userId=user._id)
p.activity-desc
+memberName(user=user)
if($eq activityType 'createBoard')
| {{_ 'activity-created' boardLabel}}.
if($eq activityType 'createList')
| {{_ 'activity-added' list.title boardLabel}}.
if($eq activityType 'archivedList')
| {{_ 'activity-archived' list.title}}.
if($eq activityType 'createCard')
| {{{_ 'activity-added' cardLink boardLabel}}}.
if($eq activityType 'archivedCard')
| {{{_ 'activity-archived' cardLink}}}.
if($eq activityType 'restoredCard')
| {{{_ 'activity-sent' cardLink boardLabel}}}.
if($eq activityType 'moveCard')
| {{{_ 'activity-moved' cardLink oldList.title list.title}}}.
if($eq activityType 'addBoardMember')
| {{{_ 'activity-added' memberLink boardLabel}}}.
if($eq activityType 'removeBoardMember')
| {{{_ 'activity-excluded' memberLink boardLabel}}}.
if($eq activityType 'joinMember')
if($eq currentUser._id member._id)
| {{{_ 'activity-joined' cardLink}}}.
else
| {{{_ 'activity-added' memberLink cardLink}}}.
if($eq activityType 'unjoinMember')
if($eq currentUser._id member._id)
| {{{_ 'activity-unjoined' cardLink}}}.
else
| {{{_ 'activity-removed' memberLink cardLink}}}.
if($eq activityType 'addComment')
| {{{_ 'activity-on' cardLink}}}
a.activity-comment(href="{{ card.absoluteUrl }}")
+viewer
= comment.text
if($eq activityType 'addAttachment')
| {{{_ 'activity-attached' attachmentLink cardLink}}}.
span.activity-meta {{ moment createdAt }}
template(name="cardActivities")
each currentCard.activities
.activity
+userAvatar(userId=user._id)
p.activity-desc
+memberName(user=user)
if($eq activityType 'createCard')
| {{_ 'activity-added' cardLabel list.title}}.
if($eq activityType 'joinMember')
if($eq currentUser._id member._id)
| {{_ 'activity-joined' cardLabel}}.
else
| {{{_ 'activity-added' cardLabel memberLink}}}.
if($eq activityType 'unjoinMember')
if($eq currentUser._id member._id)
| {{_ 'activity-unjoined' cardLabel}}.
else
| {{{_ 'activity-removed' cardLabel memberLink}}}.
if($eq activityType 'archivedCard')
| {{_ 'activity-archived' cardLabel}}.
if($eq activityType 'restoredCard')
| {{_ 'activity-sent' cardLabel boardLabel}}.
if($eq activityType 'moveCard')
| {{_ 'activity-moved' cardLabel oldList.title list.title}}.
if($eq activityType 'addAttachment')
| {{{_ 'activity-attached' attachmentLink cardLabel}}}.
if attachment.isImage
img.attachment-image-preview(src=attachment.url)
if($eq activityType 'addComment')
+inlinedForm(classNames='js-edit-comment')
+editor(autofocus=true)
= comment.text
.edit-controls
button.primary(type="submit") {{_ 'edit'}}
else
.activity-comment
+viewer
= comment.text
span.activity-meta
| {{ moment createdAt }} -
a.js-open-inlined-form {{_ "edit"}}
= ' - '
a.js-delete-comment {{_ "delete"}}
else
span.activity-meta {{ moment createdAt }}

View file

@ -49,10 +49,6 @@ BlazeComponent.extendComponent({
return TAPi18n.__('this-board');
},
cardLabel: function() {
return TAPi18n.__('this-card');
},
cardLink: function() {
var card = this.currentData().card();
return Blaze.toHTML(HTML.A({
@ -75,3 +71,35 @@ BlazeComponent.extendComponent({
}, attachment.name()));
}
}).register('activities');
BlazeComponent.extendComponent({
template: function() {
return 'cardActivities';
},
cardLabel: function() {
return TAPi18n.__('this-card');
},
events: function() {
return [{
// XXX We should use Popup.afterConfirmation here
'click .js-delete-comment': function() {
var commentId = this.currentData().commentId;
CardComments.remove(commentId);
},
'submit .js-edit-comment': function(evt) {
evt.preventDefault();
var commentText = this.currentComponent().getValue();
var commentId = Template.parentData().commentId;
if ($.trim(commentText)) {
CardComments.update(commentId, {
$set: {
text: commentText
}
});
}
}
}];
}
}).register('cardActivities');

View file

@ -0,0 +1,28 @@
.activities
clear: both
.activity
margin: 6px 0
display: flex
.member
width: 24px
height: @width
.activity-desc
flex: 1
align-self: center
margin: 0
.activity-comment
display: block
border-radius: 3px
background: white
text-decoration: none
box-shadow: 0 1px 2px rgba(0,0,0,.2)
margin-top: 5px
padding: 5px
.activity-meta
font-size: 0.8em
color: darken(white, 40%)

View file

@ -0,0 +1,8 @@
template(name="commentForm")
.new-comment.js-new-comment(
class="{{#if commentFormIsOpen}}is-open{{/if}}")
+userAvatar(userId=currentUser._id)
form.js-new-comment-form
+editor(class="js-new-comment-input")
.add-controls
button.primary.confirm.clear.js-add-comment(type="submit") {{_ 'comment'}}

View file

@ -0,0 +1,49 @@
var commentFormIsOpen = new ReactiveVar(false);
Template.commentForm.helpers({
commentFormIsOpen: function() {
return commentFormIsOpen.get();
}
});
Template.commentForm.events({
'click .js-new-comment:not(.focus)': function() {
commentFormIsOpen.set(true);
},
'submit .js-new-comment-form': function(evt, tpl) {
var input = tpl.$('.js-new-comment-input');
if ($.trim(input.val())) {
CardComments.insert({
boardId: this.boardId,
cardId: this._id,
text: input.val()
});
input.val('');
input.blur();
commentFormIsOpen.set(false);
Tracker.flush();
autosize.update(input);
}
evt.preventDefault();
},
// Pressing Ctrl+Enter should submit the form
'keydown form textarea': function(evt, tpl) {
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
tpl.find('button[type=submit]').click();
}
}
});
Template.commentForm.onDestroyed(function() {
commentFormIsOpen.set(false);
});
EscapeActions.register('inlinedForm',
function() {
commentFormIsOpen.set(false);
$('.js-new-comment-input').blur();
},
function() { return commentFormIsOpen.get(); }, {
noClickEscapeOn: '.js-new-comment'
}
);

View file

@ -8,15 +8,15 @@
top: 1px
left: -38px
&.focus
&.is-open
.member
opacity: 1
.helper
display: inline-block
.new-comment-input
min-height: 108px
textarea
min-height: 100px
color: #4d4d4d
cursor: auto
overflow: hidden
@ -25,22 +25,22 @@
.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
textarea
background-color: #fff
box-shadow: 0 1px 3px rgba(0, 0, 0, .33)
border: 0
cursor: pointer
box-shadow: 0 1px 2px rgba(0, 0, 0, .23)
color: #8c8c8c
height: 36px
margin: 4px 4px 6px 0
padding: 9px 11px
width: 100%
&:focus
cursor: auto
&:hover,
&:is-open
background-color: #fff
box-shadow: 0 1px 3px rgba(0, 0, 0, .33)
border: 0
cursor: pointer
&:is-open
cursor: auto

View file

@ -1,30 +0,0 @@
Template.cardActivities.events({
'click .js-edit-action': function(evt) {
var $this = $(evt.currentTarget);
var container = $this.parents('.phenom-comment');
// open and focus
container.addClass('editing');
container.find('textarea').focus();
},
'click .js-confirm-delete-action': function() {
CardComments.remove(this._id);
},
'submit form': function(evt) {
var $this = $(evt.currentTarget);
var container = $this.parents('.phenom-comment');
var text = container.find('textarea');
if ($.trim(text.val())) {
CardComments.update(this._id, {
$set: {
text: text.val()
}
});
// reset editing class
$('.editing').removeClass('editing');
}
evt.preventDefault();
}
});

View file

@ -1,154 +0,0 @@
<template name="boardActivities">
{{# each currentBoard.activities }}
<div class="phenom phenom-action clearfix phenom-other">
{{> userAvatar userId=user._id}}
<div class="phenom-desc">
{{ > memberName user=user }}
{{# if $eq activityType 'createBoard' }}
{{_ 'activity-created' boardLabel}}.
{{ /if }}
{{# if $eq activityType 'createList' }}
{{_ 'activity-added' list.title boardLabel}}.
{{ /if }}
{{# if $eq activityType 'archivedList' }}
{{_ 'activity-archived' list.title}}.
{{ /if }}
{{# if $eq activityType 'createCard' }}
{{{_ 'activity-added' cardLink boardLabel}}}.
{{ /if }}
{{# if $eq activityType 'archivedCard' }}
{{{_ 'activity-archived' cardLink}}}.
{{ /if }}
{{# if $eq activityType 'restoredCard' }}
{{{_ 'activity-sent' cardLink boardLabel}}}.
{{ /if }}
{{# if $eq activityType 'moveCard' }}
{{{_ 'activity-moved' cardLink oldList.title list.title}}}.
{{ /if }}
{{# if $eq activityType 'addBoardMember' }}
{{{_ 'activity-added' memberLink boardLabel}}}.
{{ /if }}
{{# if $eq activityType 'removeBoardMember' }}
{{{_ 'activity-excluded' memberLink boardLabel}}}.
{{ /if }}
{{# if $eq activityType 'joinMember' }}
{{# if $eq currentUser._id member._id }}
{{{_ 'activity-joined' cardLink}}}.
{{ else }}
{{{_ 'activity-added' memberLink cardLink}}}.
{{/if}}
{{ /if }}
{{# if $eq activityType 'unjoinMember' }}
{{# if $eq currentUser._id member._id }}
{{{_ 'activity-unjoined' cardLink}}}.
{{ else }}
{{{_ 'activity-removed' memberLink cardLink}}}.
{{/if}}
{{ /if }}
{{# if $eq activityType 'addComment' }}
<div class="phenom-desc">
{{{_ 'activity-on' cardLink}}}
<div class="action-comment markeddown">
<a href="{{ card.absoluteUrl }}" class="current-comment show tdn">
<p>{{#viewer}}{{ comment.text }}{{/viewer}}</p>
</a>
</div>
</div>
{{ /if }}
{{# if $eq activityType 'addAttachment' }}
<div class="phenom-desc">
{{{_ 'activity-attached' attachmentLink cardLink}}}.
</div>
{{ /if }}
</div>
<p class="phenom-meta quiet">
<span class="date js-hide-on-sending">
{{ moment createdAt }}
</span>
</p>
</div>
{{ /each }}
</template>
<template name="cardActivities">
{{# each currentCard.comments }}
<div class="phenom phenom-action clearfix phenom-comment">
{{> userAvatar userId=user._id}}
<form>
<div class="phenom-desc">
{{ > memberName user=user }}
<div class="action-comment markeddown">
<div class="current-comment">
{{#viewer}}{{ text }}{{/viewer}}
</div>
<textarea class="js-text" tabindex="1">{{ text }}</textarea>
</div>
</div>
<div class="edit-controls clearfix">
<input type="submit" class="primary confirm js-save-edit" value="{{_ 'save'}}" tabindex="2">
</div>
</form>
<p class="phenom-meta quiet">
<span class="date js-hide-on-sending">{{ moment createdAt }}</span>
{{# if currentUser }}
<span class="js-hide-on-sending">
- <a href="#" class="js-edit-action">{{_ "edit"}}</a>
- <a href="#" class="js-confirm-delete-action">{{_ "delete"}}</a>
</span>
{{/ if }}
</p>
</div>
{{/each}}
{{# each currentCard.activities }}
<div class="phenom phenom-action clearfix phenom-other">
{{> userAvatar userId=user._id size="extra-small" class="creator js-show-mem-menu" }}
{{ > memberName user=user }}
{{# if $eq activityType 'createCard' }}
{{_ 'activity-added' cardLabel list.title}}.
{{ /if }}
{{# if $eq activityType 'joinMember' }}
{{# if $eq currentUser._id member._id }}
{{_ 'activity-joined' cardLabel}}.
{{ else }}
{{{_ 'activity-added' cardLabel memberLink}}}.
{{/if}}
{{/if}}
{{# if $eq activityType 'unjoinMember' }}
{{# if $eq currentUser._id member._id }}
{{_ 'activity-unjoined' cardLabel}}.
{{ else }}
{{{_ 'activity-removed' cardLabel memberLink}}}.
{{/if}}
{{ /if }}
{{# if $eq activityType 'archivedCard' }}
{{_ 'activity-archived' cardLabel}}.
{{ /if }}
{{# if $eq activityType 'restoredCard' }}
{{_ 'activity-sent' cardLabel boardLabel}}.
{{/ if }}
{{# if $eq activityType 'moveCard' }}
{{_ 'activity-moved' cardLabel oldList.title list.title}}.
{{/ if }}
{{# if $eq activityType 'addAttachment' }}
{{{_ 'activity-attached' attachmentLink cardLabel}}}.
{{# if attachment.isImage }}
<img src="{{ attachment.url }}" class="attachment-image-preview">
{{/if}}
{{/ if}}
</div>
{{/each}}
</template>

View file

@ -1,12 +1,3 @@
// XXX This event list must be abstracted somewhere else.
var endTransitionEvents = [
'webkitTransitionEnd',
'otransitionend',
'oTransitionEnd',
'msTransitionEnd',
'transitionend'
].join(' ');
BlazeComponent.extendComponent({
template: function() {
return 'boardComponent';
@ -69,7 +60,7 @@ BlazeComponent.extendComponent({
flexBasis: 0,
padding: 0
});
$(lists).one(endTransitionEvents, removeNode);
$(lists).one(CSSEvents.transitionend, removeNode);
} else {
removeNode();
}

View file

@ -106,9 +106,11 @@ template(name="createBoardPopup")
p.quiet
if $eq visibility.get 'public'
span.fa.fa-globe.colorful
= " "
| {{{_ 'board-public-info'}}}
else
span.fa.fa-lock.colorful
= " "
| {{{_ 'board-private-info'}}}
a.js-change-visibility Change.
input.primary.wide(type="submit" value="{{_ 'create'}}")

View file

@ -1,5 +1,5 @@
template(name="cardDetails")
section.card-details.js-card-details: .card-details-canvas
section.card-details.js-card-details.js-perfect-scrollbar: .card-details-canvas
if cover
.card-details-cover(style="background-image: url({{ cover.url }})")
@ -42,7 +42,7 @@ template(name="cardDetails")
//- XXX We should use "editable" to avoide repetiting ourselves
if currentUser.isBoardMember
h3.card-details-item-title Description
+inlinedForm(classNames="js-card-description")
+inlinedForm(classNames="card-description js-card-description")
+editor(autofocus=true)
= description
.edit-controls.clearfix
@ -62,9 +62,13 @@ template(name="cardDetails")
if attachments.count
hr
+WindowAttachmentsModule(card=this)
if isLoaded
hr
+WindowActivityModule(card=this)
hr
h2 {{ _ 'activity'}}
if currentUser.isBoardMember
+commentForm
if isLoaded.get
+activities(card=this mode="card")
template(name="cardDetailsActionsPopup")
if currentUser.isBoardMember
@ -75,14 +79,15 @@ template(name="cardDetailsActionsPopup")
hr
ul.pop-over-list
li: a.js-copy Copy Card
li: a.js-archive Archive Card
li: a.js-delete Delete Card
unless archived
li: a.js-archive Archive Card
li: a.js-more More
template(name="moveCardPopup")
+boardLists
template(name="cardMembersPopup")
ul.pop-over-member-list
ul.pop-over-list.pop-over-member-list
each board.members
li.item(class="{{#if isCardMember}}active{{/if}}")
a.name.js-select-member(href="#")
@ -105,6 +110,17 @@ template(name="cardLabelsPopup")
span.card-label-selectable-icon.fa.fa-check
a.quiet-button.full.js-add-label {{_ 'label-create'}}
template(name="cardMorePopup")
p.quiet
span.clearfix
span {{_ 'link-card'}}
= ' '
i.fa.colorful(class="{{#if board.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
input.inline-input(type="text" readonly value="{{ rootUrl }}")
| {{_ 'added'}}
span.date(title=card.createdAt) {{ moment createdAt 'LLL' }}
a.js-delete(title="{{_ 'card-delete-notice'}}") {{_ 'delete'}}
template(name="cardDeletePopup")
p {{_ "card-delete-pop"}}
unless archived

View file

@ -4,7 +4,7 @@ BlazeComponent.extendComponent({
},
mixins: function() {
return [Mixins.InfiniteScrolling];
return [Mixins.InfiniteScrolling, Mixins.PerfectScrollbar];
},
calculateNextPeak: function() {
@ -35,8 +35,19 @@ BlazeComponent.extendComponent({
});
},
onCreated: function() {
this.isLoaded = new ReactiveVar(false);
},
events: function() {
return [{
// XXX We can't define this event directly in the event map below because we
// miss ES6 object keys interpolation.
var events = {};
events[CSSEvents.animationend + ' .js-card-details'] = function() {
this.isLoaded.set(true);
};
return [_.extend(events, {
'click .js-close-card-details': function() {
Utils.goBoardId(this.data().boardId);
},
@ -60,7 +71,7 @@ BlazeComponent.extendComponent({
'mouseenter .js-card-details': function() {
this.componentParent().showOverlay.set(true);
}
}];
})];
}
}).register('cardDetails');
@ -78,11 +89,7 @@ Template.cardDetailsActionsPopup.events({
});
Popup.close();
},
'click .js-delete': Popup.afterConfirm('cardDelete', function() {
var cardId = this._id;
Cards.remove(cardId);
Popup.close();
})
'click .js-more': Popup.open('cardMore')
});
Template.moveCardPopup.events({
@ -100,6 +107,14 @@ Template.moveCardPopup.events({
}
});
Template.cardMorePopup.events({
'click .js-delete': Popup.afterConfirm('cardDelete', function() {
Popup.close();
Cards.remove(this._id);
Utils.goBoardId(this.board()._id);
})
});
// Close the card details pane by pressing escape
EscapeActions.register('detailsPane',
function() { Utils.goBoardId(Session.get('currentBoard')); },

View file

@ -10,9 +10,9 @@
background: white
border-radius: 3px
z-index: 20 !important
animation: flexGrowIn 0.2s
animation: flexGrowIn 0.1s
box-shadow: 0 0 7px 0 darken(white, 30%)
transition: flex-basis 0.2s, padding 0.2s
transition: flex-basis 0.1s
margin-top: -9px
.card-details-canvas
@ -62,13 +62,18 @@
border-radius: 3px
padding: 0px 5px
.card-description textarea
min-height: 100px
.card-details-items
display: flex
margin: 15px 0
.card-details-item
flex-grow: 1
&.card-details-item-labels,
&.card-details-item-members
width: 50%
flex-shrink: 1
.card-details-item-title
font-size: 14px
@ -78,62 +83,8 @@
padding-top: 5px
padding-bottom: 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
.card-composer
padding-bottom: 8px
.activities
padding-top: 10px
input[type="text"].attachment-add-link-input
float: left

View file

@ -1,61 +1,3 @@
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',
@ -77,130 +19,6 @@ Template.WindowAttachmentsModule.events({
}
});
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').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').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;

View file

@ -1,48 +0,0 @@
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] : labelColors[0];
}
});
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);
}
});

View file

@ -0,0 +1,27 @@
template(name="formLabel")
.colors
label(for="labelName") {{_ 'name'}}
input.js-label-name#labelName(type="text" name="name" value=name autofocus)
label {{_ "select-color"}}
each labels
span.card-label.card-label--selectable.palette-color.js-palette-color(class="card-label-{{color}}")
if($eq color ../color)
i.fa.fa-check
template(name="createLabelPopup")
form.create-label
with(color=defaultColor)
+formLabel
button.primary.wide(type="submit") {{_ 'create'}}
template(name="editLabelPopup")
form.edit-label
+formLabel
button.primary.wide.left(type="submit") {{_ 'save'}}
span.right
template(name="deleteLabelPopup")
p {{_ "label-delete-pop"}}
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}

View file

@ -0,0 +1,126 @@
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').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').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.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] : labelColors[0];
}
});
Template.formLabel.helpers({
labels: function() {
return _.map(labelColors, function(color) {
return { color: color, name: '' };
});
}
});

View file

@ -85,10 +85,6 @@
left: 0
width: 260px
.editable-labels .card-label:hover
cursor: pointer
opacity: .75
.edit-labels-pop-over
margin-bottom: 8px
@ -98,7 +94,9 @@
.card-label-selectable
border-radius: 3px
cursor: pointer
margin: 0 50px 4px 0
margin: 0
margin-bottom: 3px
width: 190px
min-height: 18px
padding: 8px
position: relative
@ -113,21 +111,13 @@
&.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

View file

@ -15,7 +15,7 @@ template(name="minicard")
if comments.count
.badge(title="{{_ 'card-comments-title' comments.count }}")
span.badge-icon.fa.fa-comment-o
.badge-text= comments.count
span.badge-text= comments.count
if description
.badge.badge-state-image-only(title=description)
span.badge-icon.fa.fa-align-left

View file

@ -78,20 +78,34 @@
margin-bottom: 2px
text-decoration: none
word-wrap: break-word
clear: both
&::selection
background: transparent
.minicard-labels
float: right
display: flex
.minicard-label
float: right
width: 11px
height: @width
border-radius: 2px
margin-right: 3px
margin-left: 3px
.badges
float: left
margin-top: 5px
color: darken(white, 60%)
&:empty
display: none
.badge
float: left
margin-right: 10px
.badge-text
font-size: 0.9em
.minicard-members
float: right
@ -109,12 +123,6 @@
.minicard-members:empty
display: none
.badges
float: left
&:empty
display: none
&.minicard-composer
margin-bottom: 10px

View file

@ -1,34 +1,3 @@
<template name="cardMemberPopup">
<div class="board-member-menu">
<div class="mini-profile-info">
{{> userAvatar userId=user._id }}
<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="cardAttachmentsPopup">
<div>
<ul class="pop-over-list">
@ -42,43 +11,6 @@
</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="attachmentDeletePopup">
<p>{{_ "attachment-delete-pop"}}</p>
<input type="submit" class="js-confirm negate full" value="{{_ 'delete'}}">
@ -263,45 +195,3 @@
</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>

View file

@ -95,6 +95,11 @@ textarea
resize: vertical
width: 100%
&.editor
resize: none
padding-bottom: 22px
.button
border-radius: 3px
text-decoration: none

View file

@ -1,2 +0,0 @@
textarea.editor
min-height: 100px

View file

@ -1,22 +1,3 @@
// XXX This event list must be abstracted somewhere else.
function whichTransitionEvent() {
var t;
var el = document.createElement('fakeelement');
var transitions = {
transition:'transitionend',
OTransition:'oTransitionEnd',
MozTransition:'transitionend',
WebkitTransition:'webkitTransitionEnd'
};
for (t in transitions) {
if (el.style[t] !== undefined) {
return transitions[t];
}
}
}
var transitionEvent = whichTransitionEvent();
Popup.template.events({
'click .js-back-view': function() {
Popup.back();
@ -50,7 +31,7 @@ Popup.template.onRendered(function() {
container._uihooks = {
removeElement: function(node) {
$(node).addClass('no-height');
$(container).one(transitionEvent, function() {
$(container).one(CSSEvents.transitionend, function() {
node.parentNode.removeChild(node);
});
}

View file

@ -19,14 +19,17 @@ template(name="userPopup")
+userAvatar(userId=user._id)
.info
h3.bottom
a.js-profile(href="{{pathFor route='Profile' username=user.username}}")= user.profile.name
= user.profile.fullname
p.quiet.bottom @{{ user.username }}
template(name="memberName")
a.js-show-mem-menu(href="{{pathFor route='Profile' username=user.username}}")
if showBoth
= user.profile.fullname
if username
| ({{ user.username }})
| ({{ user.username }})
else if user.profile.fullname
= user.profile.fullname
else
= user.username
template(name="changeAvatarPopup")
ul.pop-over-list
@ -54,3 +57,14 @@ template(name="changeAvatarPopup")
button.full.js-upload-avatar
i.fa.fa-upload
| Upload an avatar
template(name="cardMemberPopup")
.board-member-menu
.mini-profile-info
+userAvatar(userId=user._id)
.info
h3.bottom= user.profile.fullname
p.quiet.bottom @{{ user.username }}
if currentUser.isBoardMember
ul.pop-over-list
li: a.js-remove-member {{_ 'remove-member-from-card'}}

View file

@ -111,3 +111,40 @@ BlazeComponent.extendComponent({
}];
}
}).register('changeAvatarPopup');
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.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.cardMemberPopup.events({
'click .js-remove-member': function() {
Cards.update(this.cardId, {$pull: {members: this.userId}});
Popup.close();
}
});

6
client/config/helpers.js Normal file
View file

@ -0,0 +1,6 @@
Blaze.registerHelper('currentCard', function() {
var cardId = Session.get('currentCard');
if (cardId) {
return Cards.findOne(cardId);
}
});

42
client/lib/cssEvents.js Normal file
View file

@ -0,0 +1,42 @@
// XXX Should we use something like Moderniz instead of our custom detector?
var whichTransitionEvent = function() {
var t;
var el = document.createElement('fakeelement');
var transitions = {
transition:'transitionend',
OTransition:'oTransitionEnd',
MSTransition:'msTransitionEnd',
MozTransition:'transitionend',
WebkitTransition:'webkitTransitionEnd'
};
for (t in transitions) {
if (el.style[t] !== undefined) {
return transitions[t];
}
}
};
var whichAnimationEvent = function() {
var t;
var el = document.createElement('fakeelement');
var transitions = {
animation:'animationend',
OAnimation:'oAnimationEnd',
MSTransition:'msAnimationEnd',
MozAnimation:'animationend',
WebkitAnimation:'webkitAnimationEnd'
};
for (t in transitions) {
if (el.style[t] !== undefined) {
return transitions[t];
}
}
};
CSSEvents = {
transitionend: whichTransitionEvent(),
animationend: whichAnimationEvent()
};

View file

@ -77,6 +77,9 @@ a
cursor: default
text-decoration: none
strong
font-weight: bold
p
a
text-decoration: underline

View file

@ -130,8 +130,7 @@ Cards.helpers({
return _.contains(this.members, memberId);
},
activities: function() {
return Activities.find({ type: 'card', cardId: this._id },
{ sort: { createdAt: -1 }});
return Activities.find({ cardId: this._id }, { sort: { createdAt: -1 }});
},
comments: function() {
return CardComments.find({ cardId: this._id }, { sort: { createdAt: -1 }});
@ -182,7 +181,6 @@ CardComments.before.insert(function(userId, doc) {
if (Meteor.isServer) {
Cards.after.insert(function(userId, doc) {
Activities.insert({
type: 'card',
activityType: 'createCard',
boardId: doc.boardId,
listId: doc.listId,
@ -196,7 +194,6 @@ if (Meteor.isServer) {
if (_.contains(fieldNames, 'archived')) {
if (doc.archived) {
Activities.insert({
type: 'card',
activityType: 'archivedCard',
boardId: doc.boardId,
listId: doc.listId,
@ -205,7 +202,6 @@ if (Meteor.isServer) {
});
} else {
Activities.insert({
type: 'card',
activityType: 'restoredCard',
boardId: doc.boardId,
listId: doc.listId,
@ -221,7 +217,6 @@ if (Meteor.isServer) {
var oldListId = this.previous.listId;
if (_.contains(fieldNames, 'listId') && doc.listId !== oldListId) {
Activities.insert({
type: 'card',
activityType: 'moveCard',
listId: doc.listId,
oldListId: oldListId,
@ -242,7 +237,6 @@ if (Meteor.isServer) {
memberId = modifier.$addToSet.members;
if (! _.contains(doc.members, memberId)) {
Activities.insert({
type: 'card',
activityType: 'joinMember',
boardId: doc.boardId,
cardId: doc._id,
@ -256,7 +250,6 @@ if (Meteor.isServer) {
if (modifier.$pull && modifier.$pull.members) {
memberId = modifier.$pull.members;
Activities.insert({
type: 'card',
activityType: 'unjoinMember',
boardId: doc.boardId,
cardId: doc._id,
@ -275,7 +268,6 @@ if (Meteor.isServer) {
CardComments.after.insert(function(userId, doc) {
Activities.insert({
type: 'comment',
activityType: 'addComment',
boardId: doc.boardId,
cardId: doc.cardId,