diff options
Diffstat (limited to 'client')
90 files changed, 7695 insertions, 0 deletions
diff --git a/client/components/activities/activities.jade b/client/components/activities/activities.jade new file mode 100644 index 00000000..1c6b9faf --- /dev/null +++ b/client/components/activities/activities.jade @@ -0,0 +1,8 @@ +template(name="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 diff --git a/client/components/activities/activities.js b/client/components/activities/activities.js new file mode 100644 index 00000000..c806e87b --- /dev/null +++ b/client/components/activities/activities.js @@ -0,0 +1,77 @@ +var activitiesPerPage = 20; + +BlazeComponent.extendComponent({ + template: function() { + return 'activities'; + }, + + onCreated: function() { + var self = this; + // XXX Should we use ReactiveNumber? + self.page = new ReactiveVar(1); + self.loadNextPageLocked = false; + var sidebar = self.componentParent(); // XXX for some reason not working + sidebar.callFirstWith(null, 'resetNextPeak'); + self.autorun(function() { + var mode = self.data().mode; + var capitalizedMode = Utils.capitalize(mode); + var id = Session.get('current' + capitalizedMode); + var limit = self.page.get() * activitiesPerPage; + if (id === null) + return; + + self.subscribe('activities', mode, id, limit, function() { + self.loadNextPageLocked = false; + + // If the sibear peak hasn't increased, that mean that there are no more + // activities, and we can stop calling new subscriptions. + // XXX This is hacky! We need to know excatly and reactively how many + // activities there are, we probably want to denormalize this number + // dirrectly into card and board documents. + var a = sidebar.callFirstWith(null, 'getNextPeak'); + sidebar.calculateNextPeak(); + var b = sidebar.callFirstWith(null, 'getNextPeak'); + if (a === b) { + sidebar.callFirstWith(null, 'resetNextPeak'); + } + }); + }); + }, + + loadNextPage: function() { + if (this.loadNextPageLocked === false) { + this.page.set(this.page.get() + 1); + this.loadNextPageLocked = true; + } + }, + + boardLabel: function() { + return TAPi18n.__('this-board'); + }, + + cardLabel: function() { + return TAPi18n.__('this-card'); + }, + + cardLink: function() { + var card = this.currentData().card(); + return Blaze.toHTML(HTML.A({ + href: card.absoluteUrl(), + 'class': 'action-card' + }, card.title)); + }, + + memberLink: function() { + return Blaze.toHTMLWithData(Template.memberName, { + user: this.currentData().member() + }); + }, + + attachmentLink: function() { + var attachment = this.currentData().attachment(); + return Blaze.toHTML(HTML.A({ + href: attachment.url(), + 'class': 'js-open-attachment-viewer' + }, attachment.name())); + } +}).register('activities'); diff --git a/client/components/activities/comments.jade b/client/components/activities/comments.jade new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/client/components/activities/comments.jade diff --git a/client/components/activities/comments.js b/client/components/activities/comments.js new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/client/components/activities/comments.js diff --git a/client/components/activities/events.js b/client/components/activities/events.js new file mode 100644 index 00000000..ea98e65f --- /dev/null +++ b/client/components/activities/events.js @@ -0,0 +1,30 @@ +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(); + } +}); diff --git a/client/components/activities/templates.html b/client/components/activities/templates.html new file mode 100644 index 00000000..8d3ff763 --- /dev/null +++ b/client/components/activities/templates.html @@ -0,0 +1,154 @@ +<template name="boardActivities"> + {{# each currentBoard.activities }} + <div class="phenom phenom-action clearfix phenom-other"> + {{> userAvatar user=user size="extra-small" class="creator js-show-mem-menu" }} + <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 user=user size="small" class="creator js-show-mem-menu" }} + <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 user=user 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> diff --git a/client/components/boards/body.jade b/client/components/boards/body.jade new file mode 100644 index 00000000..5406ee2f --- /dev/null +++ b/client/components/boards/body.jade @@ -0,0 +1,33 @@ +//- + XXX This template can't be transformed into a component because it is + included by iron-router. That's a bug. +template(name="board") + +boardComponent + +template(name="boardComponent") + if this + .board-wrapper(class=colorClass) + .board-canvas(class=sidebarSize) + .lists.js-lists + each lists + +list(this) + if currentUser.isBoardMember + +addlistForm + +boardSidebar + if currentCard + +cardSidebar(currentCard) + else + +message(label="board-no-found") + +template(name="addlistForm") + .list.js-list.add-list.js-add-list + +inlinedForm(autoclose=false) + input.list-name-input(type="text" placeholder="{{_ 'add-list'}}" + autocomplete="off" autofocus) + div.edit-controls.clearfix + button.primary.confirm.js-save-edit(type="submit") {{_ 'save'}} + a.fa.fa-times.dark-hover.cancel.js-close-inlined-form + else + .js-open-inlined-form + i.fa.fa-plus + | {{_ 'add-list'}} diff --git a/client/components/boards/body.js b/client/components/boards/body.js new file mode 100644 index 00000000..2b4baf53 --- /dev/null +++ b/client/components/boards/body.js @@ -0,0 +1,70 @@ +BlazeComponent.extendComponent({ + template: function() { + return 'boardComponent'; + }, + + openNewListForm: function() { + this.componentChildren('addlistForm')[0].open(); + }, + + scrollLeft: function() { + // TODO + }, + + onRendered: function() { + var self = this; + + self.scrollLeft(); + + if (Meteor.user().isBoardMember()) { + self.$('.js-lists').sortable({ + tolerance: 'pointer', + appendTo: '.js-lists', + helper: 'clone', + items: '.js-list:not(.add-list)', + placeholder: 'list placeholder', + start: function(event, ui) { + $('.list.placeholder').height(ui.item.height()); + Popup.close(); + }, + stop: function() { + self.$('.js-lists').find('.js-list:not(.add-list)').each( + function(i, list) { + var data = Blaze.getData(list); + Lists.update(data._id, { + $set: { + sort: i + } + }); + } + ); + } + }); + + // If there is no data in the board (ie, no lists) we autofocus the list + // creation form by clicking on the corresponding element. + if (self.data().lists().count() === 0) { + this.openNewListForm(); + } + } + }, + + sidebarSize: function() { + var sidebar = this.componentChildren('boardSidebar')[0]; + if (Session.get('currentCard') !== null) + return 'next-large-sidebar'; + else if (sidebar && sidebar.isOpen()) + return 'next-small-sidebar'; + } +}).register('boardComponent'); + +BlazeComponent.extendComponent({ + template: function() { + return 'addlistForm'; + }, + + // Proxy + open: function() { + this.componentChildren('inlinedForm')[0].open(); + } +}).register('addlistForm'); diff --git a/client/components/boards/body.styl b/client/components/boards/body.styl new file mode 100644 index 00000000..cb351e46 --- /dev/null +++ b/client/components/boards/body.styl @@ -0,0 +1,54 @@ +@import 'nib' + +.board-wrapper + left: 0 + top: 0 + bottom: 0 + right: 0 + position: absolute + overflow: hidden + + .board-canvas + position: absolute + left: 0 + right: 0 + top: 0 + bottom: 0 + transition: margin .1s + + &.next-small-sidebar + margin-right: 248px + + &.next-large-sidebar + opacity: 0.8 + margin-right: 496px + +.lists + align-items: flex-start + display: flex + flex-direction: row + margin-bottom: 10px + overflow-x: auto + overflow-y: hidden + padding-bottom: 10px + position: absolute + top: 0 + right: 0 + bottom: 0 + left: 0 + + &::-webkit-scrollbar + height: 13px + width: 13px + + &::-webkit-scrollbar-thumb:vertical, + &::-webkit-scrollbar-thumb:horizontal + background: rgba(255, 255, 255, .4) + + &::-webkit-scrollbar-track-piece + background: rgba(0, 0, 0, .15) + + &::-webkit-scrollbar-button + display: block + height: 5px + width: 5px diff --git a/client/components/boards/colors.styl b/client/components/boards/colors.styl new file mode 100644 index 00000000..1db44845 --- /dev/null +++ b/client/components/boards/colors.styl @@ -0,0 +1,34 @@ +// We define a set of six board colors that we took from the FlatUI palette. +// http://flatuicolors.com + +setBoardColor(color) + &#header, + &.sk-spinner div, + .board-backgrounds-list &.background-box, + &.pop-over .pop-over-list li a:hover, + .board-list & a + background-color: color + + & .minicard.is-selected .minicard-details + border-bottom: 2px solid color + + button[type=submit].primary, input[type=submit].primary + background-color: darken(color, 20%) + +.board-color-nephritis + setBoardColor(#27AE60) + +.board-color-pomegranate + setBoardColor(#C0392B) + +.board-color-belize + setBoardColor(#2980B9) + +.board-color-wisteria + setBoardColor(#8E44AD) + +.board-color-midnight + setBoardColor(#2C3E50) + +.board-color-pumpkin + setBoardColor(#E67E22) diff --git a/client/components/boards/events.js b/client/components/boards/events.js new file mode 100644 index 00000000..6f9d7fc6 --- /dev/null +++ b/client/components/boards/events.js @@ -0,0 +1,96 @@ +var toggleBoardStar = function(boardId) { + var queryType = Meteor.user().hasStarred(boardId) ? '$pull' : '$addToSet'; + var query = {}; + query[queryType] = { + 'profile.starredBoards': boardId + }; + Meteor.users.update(Meteor.userId(), query); +}; + +Template.boards.events({ + 'click .js-star-board': function(evt) { + toggleBoardStar(this._id); + evt.preventDefault(); + } +}); + +Template.headerBoard.events({ + 'click .js-star-board': function() { + toggleBoardStar(this._id); + }, + 'click .js-open-board-menu': Popup.open('boardMenu'), + 'click #permission-level:not(.no-edit)': Popup.open('boardChangePermission'), + 'click .js-filter-cards-indicator': function(evt) { + Session.set('currentWidget', 'filter'); + evt.preventDefault(); + }, + 'click .js-filter-card-clear': function(evt) { + Filter.reset(); + evt.stopPropagation(); + } +}); + +Template.boardMenuPopup.events({ + 'click .js-rename-board': Popup.open('boardChangeTitle'), + 'click .js-change-board-color': Popup.open('boardChangeColor') +}); + +Template.createBoardPopup.events({ + 'submit #CreateBoardForm': function(evt, t) { + var title = t.$('#boardNewTitle'); + + // trim value title + if ($.trim(title.val())) { + // İnsert Board title + var boardId = Boards.insert({ + title: title.val(), + permission: 'public' + }); + + // Go to Board _id + Utils.goBoardId(boardId); + } + evt.preventDefault(); + } +}); + +Template.boardChangeTitlePopup.events({ + 'submit #ChangeBoardTitleForm': function(evt, t) { + var title = t.$('.js-board-name').val().trim(); + if (title) { + Boards.update(this._id, { + $set: { + title: title + } + }); + Popup.close(); + } + evt.preventDefault(); + } +}); + +Template.boardChangePermissionPopup.events({ + 'click .js-select': function(evt) { + var $this = $(evt.currentTarget); + var permission = $this.attr('name'); + + Boards.update(this._id, { + $set: { + permission: permission + } + }); + Popup.close(); + } +}); + +Template.boardChangeColorPopup.events({ + 'click .js-select-background': function(evt) { + var currentBoardId = Session.get('currentBoard'); + Boards.update(currentBoardId, { + $set: { + color: this.toString() + } + }); + evt.preventDefault(); + } +}); diff --git a/client/components/boards/header.jade b/client/components/boards/header.jade new file mode 100644 index 00000000..189cdac4 --- /dev/null +++ b/client/components/boards/header.jade @@ -0,0 +1,87 @@ +template(name="headerBoard") + h1.header-board-menu.js-open-board-menu + = title + span.fa.fa-angle-down + + .board-header-btns.left + unless isSandstorm + a.board-header-btn.js-star-board(class="{{#if isStarred}}board-header-starred{{/if}}" + title="{{# if isStarred }}{{_ 'click-to-unstar'}}{{ else }}{{_ 'click-to-star'}}{{/ if }} {{_ 'starred-boards-description'}}") + span.board-header-btn-icon.icon-sm.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}") + //- XXX To implement + span.board-header-btn-text Starred + //- + XXX Normally we would disable this field for sandstorm, but we keep it + until sandstorm implements sharing capabilities + + a.board-header-btn.perms-btn.js-change-vis(class="{{#unless currentUser.isBoardAdmin}}no-edit{{/ unless}}" id="permission-level") + span.board-header-btn-icon.icon-sm.fa(class="{{#if isPublic}}fa-globe{{else}}fa-lock{{/if}}") + span.board-header-btn-text {{_ permission}} + + a.board-header-btn.js-search + span.board-header-btn-icon.icon-sm.fa.fa-tag + span.board-header-btn-text Labels + + //- XXX Clicking here should open a search field + a.board-header-btn.js-search + span.board-header-btn-icon.icon-sm.fa.fa-search + span.board-header-btn-text {{_ 'search'}} + + //- +boardMembersHeader + +template(name="boardMembersHeader") + .board-header-members + each currentBoard.members + +userAvatar(userId=userId draggable=true showBadges=true) + unless isSandstorm + if currentUser.isBoardAdmin + a.member.add-board-member.js-open-manage-board-members + i.fa.fa-plus + +template(name="boardMenuPopup") + ul.pop-over-list + li: a.js-rename-board {{_ 'rename-board'}} + li: a.js-change-board-color Change color + li: a Copy this board + li: a Rules + +template(name="boardChangeTitlePopup") + form#ChangeBoardTitleForm + label {{_ 'name'}} + input.js-board-name(type="text" value="{{ title }}" autofocus) + input.primary.wide.js-rename-board(type="submit" value="{{_ 'rename'}}") + +template(name="boardChangePermissionPopup") + ul.pop-over-list + li + a.js-select.light-hover(name="private") + span.icon-sm.fa.fa-lock.vis-icon + | {{_ 'private'}} + if check 'private' + span.icon-sm.fa.fa-check + span.sub-name {{_ 'private-desc'}} + li + a.js-select.light-hover(name="public") + span.icon-sm.fa.fa-globe.vis-icon + | {{_ 'public'}} + if check 'public' + span.icon-sm.fa.fa-check + span.sub-name {{_ 'public-desc'}} + +template(name="boardChangeColorPopup") + .board-backgrounds-list.clearfix + each backgroundColors + .board-background-select.js-select-background + span.background-box(class="board-color-{{this}}") + if isSelected + i.fa.fa-check + +template(name="createBoardPopup") + .content.clearfix + form#CreateBoardForm + label(for="boardNewTitle") {{_ 'title'}} + input#boardNewTitle.non-empty(type="text" name="name" placeholder="{{_ 'bucket-example'}}" autofocus) + p.quiet + span.icon-sm.fa.fa-globe + | {{{_ 'board-public-info'}}} + input.primary.wide(type="submit" value="{{_ 'create'}}") diff --git a/client/components/boards/header.js b/client/components/boards/header.js new file mode 100644 index 00000000..7d02df48 --- /dev/null +++ b/client/components/boards/header.js @@ -0,0 +1,7 @@ +Template.headerBoard.helpers({ + isStarred: function() { + var boardId = Session.get('currentBoard'); + var user = Meteor.user(); + return boardId && user && user.hasStarred(boardId); + } +}); diff --git a/client/components/boards/header.styl b/client/components/boards/header.styl new file mode 100644 index 00000000..44c38a4b --- /dev/null +++ b/client/components/boards/header.styl @@ -0,0 +1,137 @@ +@import 'nib' + +.board-header { + height: auto; + overflow: hidden; + padding: 10px 30px 10px 8px; + position: relative; + transition: padding .15s ease-in; +} + +.board-header-btns { + position: relative; + display: block; +} + +.board-header-btn { + border-radius: 3px; + color: #f6f6f6; + cursor: default; + float: left; + font-size: 12px; + height: 30px; + line-height: 32px; + margin: 2px 4px 0 0; + overflow: hidden; + padding-left: 30px; + position: relative; + text-decoration: none; +} + +.board-header-btn:empty { + display: none; +} + +.board-header-btn-without-icon { + padding-left: 8px; +} + +.board-header-btn-icon { + background-clip: content-box; + background-origin: content-box; + color: #f6f6f6 !important; + padding: 6px; + position: absolute; + top: 0; + left: 0; +} + +.board-header-btn-text { + padding-right: 8px; +} + +.board-header-btn:not(.no-edit) .text { + text-decoration: underline; +} + +.board-header-btn:not(.no-edit):hover { + background: rgba(0, 0, 0, .12); + cursor: pointer; +} + +.board-header-btn:hover { + color: #f6f6f6; +} + +.board-header-btn.board-header-btn-enabled { + background-color: rgba(0, 0, 0, .1); + + &:hover { + background-color: rgba(0, 0, 0, .3); + } + + .board-header-btn-icon.icon-star { + color: #e6bf00 !important; + } +} + +.board-header-btn-name { + cursor: default; + font-size: 18px; + font-weight: 700; + line-height: 30px; + padding-left: 4px; + text-decoration: none; + + .board-header-btn-text { + padding-left: 6px; + } +} + +.board-header-btn-name-org-logo { + border-radius: 3px; + height: 30px; + left: 0; + position: absolute; + top: 0; + width: 30px; + + .board-header-btn-text { + padding-left: 32px; + } +} + +.board-header-btn-org-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 400px; +} + +.board-header-btn-filter-indicator { + background: #3d990f; + padding-right: 30px; + color: #fff; + text-shadow: 0; + + &:hover { + background: #43a711 !important; + } + + .board-header-btn-icon-close { + background: #43a711; + border-top-left-radius: 0; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 0; + color: #fff; + padding: 6px; + position: absolute; + right: 0; + top: 0; + + &:hover { + background: #48b512; + } + } +} diff --git a/client/components/boards/helpers.js b/client/components/boards/helpers.js new file mode 100644 index 00000000..05be987d --- /dev/null +++ b/client/components/boards/helpers.js @@ -0,0 +1,45 @@ +Template.boards.helpers({ + boards: function() { + return Boards.find({}, { + sort: ['title'] + }); + }, + + starredBoards: function() { + var cursor = Boards.find({ + _id: { $in: Meteor.user().profile.starredBoards || [] } + }, { + sort: ['title'] + }); + return cursor.count() === 0 ? null : cursor; + }, + + isStarred: function() { + var user = Meteor.user(); + return user && user.hasStarred(this._id); + } +}); + +Template.boardChangePermissionPopup.helpers({ + check: function(perm) { + return this.permission === perm; + } +}); + +Template.boardChangeColorPopup.helpers({ + backgroundColors: function() { + return Boards.simpleSchema()._schema.color.allowedValues; + }, + + isSelected: function() { + var currentBoard = Boards.findOne(Session.get('currentBoard')); + return currentBoard.color === this.toString(); + } +}); + +Blaze.registerHelper('currentBoard', function() { + var boardId = Session.get('currentBoard'); + if (boardId) { + return Boards.findOne(boardId); + } +}); diff --git a/client/components/boards/list.jade b/client/components/boards/list.jade new file mode 100644 index 00000000..3a8fecd2 --- /dev/null +++ b/client/components/boards/list.jade @@ -0,0 +1,14 @@ +template(name="boards") + if boards + ul.board-list.clearfix + each boards + li(class="{{#if isStarred}}starred{{/if}}" class=colorClass) + a.js-open-board(href="{{ pathFor route='Board' boardId=_id }}") + span.details + span.board-list-item-name= title + i.fa.fa-star-o.js-star-board( + class="{{#if isStarred}}is-star-active{{/if}}" + title="{{_ 'star-board-title'}}") + else + p.quiet {{_ 'no-boards'}} + button.js-add-board {{_ 'add-board'}} diff --git a/client/components/boards/list.styl b/client/components/boards/list.styl new file mode 100644 index 00000000..c068dbb0 --- /dev/null +++ b/client/components/boards/list.styl @@ -0,0 +1,85 @@ +.board-list + margin: 25px auto + width: 1200px + + li + float: left + width: 25% + box-sizing: border-box + position: relative + + &.starred .fa-star-o + opacity: 1 + + a + background-color: #999 + color: #f6f6f6 + height: 90px + font-size: 16px + line-height: 22px + border-radius: 3px + display: block + font-weight: 700 + min-height: 18px + padding: 8px 12px 8px 12px + margin: 0 16px 16px 0 + position: relative + text-decoration: none + + &.tile + background-size: auto + background-repeat: repeat + + .details + height: 84px + padding-right: 36px + bottom: 0 + left: 0 + overflow: hidden + padding: 9px 12px + position: absolute + right: 0 + top: 0 + + .board-list-item-sub-name + color: rgba(255, 255, 255, .5) + display: block + font-size: 14px + font-weight: 400 + line-height: 22px + + .fa-star-o + bottom: 0 + font-size: 14px + height: 18px + line-height: 18px + opacity: 0 + padding: 9px 9px + position: absolute + right: 0 + top: 0 + transition-duration: .15s + transition-property: color, font-size, background + + .is-star-active + color: #e6bf00 + + li:hover a + color: #f6f6f6 + + .fa-star-o + color: #fff + opacity: .75 + + &:hover + font-size: 18px + opacity: 1 + + &.is-star-active + color: #e6bf00 + opacity: 1 + + &:hover + color: #ffd91a + font-size: 16px + opacity: 1 diff --git a/client/components/boards/router.js b/client/components/boards/router.js new file mode 100644 index 00000000..6845b7f2 --- /dev/null +++ b/client/components/boards/router.js @@ -0,0 +1,34 @@ +Meteor.subscribe('boards'); + +BoardSubsManager = new SubsManager(); + +Router.route('/boards', { + name: 'Boards', + template: 'boards', + authenticated: true, + onBeforeAction: function() { + Session.set('currentBoard', ''); + Filter.reset(); + this.next(); + } +}); + +Router.route('/boards/:_id/:slug', { + name: 'Board', + template: 'board', + onAfterAction: function() { + Session.set('sidebarIsOpen', true); + Session.set('currentWidget', 'home'); + Session.set('menuWidgetIsOpen', false); + }, + waitOn: function() { + var params = this.params; + Session.set('currentBoard', params._id); + Session.set('currentCard', null); + + return BoardSubsManager.subscribe('board', params._id, params.slug); + }, + data: function() { + return Boards.findOne(this.params._id); + } +}); 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> diff --git a/client/components/forms/cachedValue.js b/client/components/forms/cachedValue.js new file mode 100644 index 00000000..a2898d85 --- /dev/null +++ b/client/components/forms/cachedValue.js @@ -0,0 +1,22 @@ +var emptyValue = ''; + +Mixins.CachedValue = BlazeComponent.extendComponent({ + onCreated: function() { + this._cachedValue = emptyValue; + }, + + setCache: function(value) { + this._cachedValue = value; + }, + + getCache: function(defaultValue) { + if (this._cachedValue === emptyValue) + return defaultValue || ''; + else + return this._cachedValue; + }, + + resetCache: function() { + this.setCache(''); + } +}); diff --git a/client/components/forms/forms.styl b/client/components/forms/forms.styl new file mode 100644 index 00000000..1084a4a6 --- /dev/null +++ b/client/components/forms/forms.styl @@ -0,0 +1,636 @@ +@import 'nib' + +textarea, +input:not([type=file]), +button + box-sizing: border-box + -webkit-appearance: none + background-color: #ebebeb + border: 1px solid #ccc + border-radius: 3px + display: block + margin-bottom: 12px + min-height: 34px + padding: 7px + + &.full + width: 100% + + &.input-error + background-color: #ece9e9 + border-color: #ba1212 + + &:focus + outline: 0 + +input[type="file"] + margin-bottom: 16px + +input[type="radio"] + -webkit-appearance: radio + min-height: inherit + +input[type="checkbox"] + -webkit-appearance: checkbox + margin-right: 4px + +input[type="text"], +input[type="password"], +input[type="email"] + transition: background 85ms ease-in, + border-color 85ms ease-in + width: 250px + + &.inline-input + background: none + border: 0 + margin: 0 + padding: 2px + min-height: 0 + height: 18px + width: 200px + +input[type="email"]:invalid + box-shadow: none + +input[type="text"], +input[type="password"], +input[type="email"], +textarea + + &:hover + border-color: #999 + + &.input-error + border-color: #ba1212 + + &:focus + background: #fff + border-color: #318ec4 + box-shadow: 0 0 2px #318ec4 + + &.input-error + background-color: #f8f7f7 + border-color: #ba1212 + box-shadow: 0 0 2px #d11515 + + &:disabled + background-color: #dcdcdc + border-color: #bfbfbf + color: #8c8c8c + -webkit-touch-callout: none + user-select: none + +select + max-height: 300px + width: 256px + margin-bottom: 8px + +option[disabled] + color: #8c8c8c + +textarea + height: 150px + transition: background 85ms ease-in, + border-color 85ms ease-in + resize: vertical + width: 100% + +.button + border-radius: 3px + text-decoration: none + position: relative + +input[type="submit"], +button + background: #cfcfcf + background: linear-gradient(#cfcfcf, #c2c2c2) + border: none + box-shadow: 0 1px 0 #8c8c8c + cursor: pointer + display: inline-block + font-weight: 700 + line-height: 22px + margin: 8px 4px 0 0 + padding: 7px 20px + text-align: center + + .wide + padding-left: 30px + padding-right: 30px + + &:hover, + &:focus + background: #c2c2c2 + background: linear-gradient(#c2c2c2, #b5b5b5) + + &:active + background: #b5b5b5 + background: linear-gradient(#b5b5b5, #a8a8a8) + box-shadow: inset 0 3px 6px rgba(0, 0, 0, .1) + + &:hover, + &:focus, + &:active + background: #e6e6e6 + background: linear-gradient(#e6e6e6, #e6e6e6) + + &.primary + background: #005377 + box-shadow: 0 1px 0 #4d4d4d + color: white + + &:hover, + &:focus + background: #004766 + + &:active + background: #01628C + + &.negate + &:hover, + &:focus + background: #990f0f + background: linear-gradient(#990f0f, #7d0c0c) + box-shadow: 0 1px 0 #4d4d4d + color: #fff + + &:active + background: #7d0c0c + box-shadow: 0 1px 0 #4d4d4d + color: #fff + +input[type="submit"].disabled, +input[type="submit"]:disabled, +input[type="button"].disabled, +button.disabled, +.button.disabled + + &, + &:hover, + &:active + background: #cfcfcf + cursor: default + box-shadow: none + color: #a8a8a8 + +fieldset + border: 1px solid #bfbfbf + padding: 15px + margin-bottom: 15px + +input[type="hidden"] + display: none + +input[type="checkbox"], +input[type="radio"] + display: inline + +.radio-div, +.check-div + display: block + margin: 0 0 4px 20px + min-height: 20px + position: relative + + input + left: -18px + min-height: 0 + margin: 0 + padding: 0 + position: absolute + top: 2px + + label + font-weight: 400 + +label + display: block + font-weight: 700 + margin-bottom: 4px + + &.form-error + color: #ba1212 + +input, +textarea + &::-webkit-input-placeholder, + &::-moz-placeholder + color: #8c8c8c + +.edit-controls, +.add-controls + margin-top: 0 + + button[type=submit] + float: left + height: 32px + margin-top: -2px + padding-top: 5px + padding-bottom: 5px + + i.fa.fa-times + font-size: 20px + + .option + border-color: transparent + border-radius: 3px + color: #8c8c8c + display: block + float: right + height: 30px + line-height: 30px + padding: 0 8px + margin: 0 2px + + &:hover + background-color: #dbdbdb + color: #4d4d4d + + &:active + background-color: #ccc + +.button-link + background: #fff + background: linear-gradient(#fff, #f5f5f5) + border-radius: 3px + box-sizing: border-box + user-select: none + border: 1px solid #e3e3e3 + border-bottom-color: #c2c2c2 + cursor: pointer + display: block + font-weight: 700 + height: 34px + margin-top: 6px + max-width: 300px + padding: 7px + position: relative + text-decoration: none + overflow: ellipsis + + .on + background: #48b512 + background: linear-gradient(#48b512, #3d990f) + border-radius: 3px + color: #fff + display: none + font-size: 12px + font-weight: 700 + height: 17px + line-height: @height + margin: 0 + padding: 2px 4px + position: absolute + right: 5px + top: 5px + text-align: center + + &.is-on + padding-right: 30px + max-width: 196px + + .on + display: block + + &.inline + color: #666 + padding: 2px 14px + margin-left: 4px + + &.setting + height: 52px + float: left + position: relative + margin-top: 0 + + &.disabled + background: #fff + border-color: #e9e9e9 + color: #8c8c8c + cursor: default + + select + display: none + + &:hover .label + color: #8c8c8c + + &, + &:hover, + &:active, + &.primary, + &.primary:hover, + &.primary:active + background: #cfcfcf + border-color: #c2c2c2 + border-bottom-color: #b5b5b5 + cursor: default + box-shadow: none + color: #a8a8a8 + + .label + color: #8c8c8c + display: block + font-size: 12px + line-height: 14px + margin-bottom: 0 + + &:hover .label + color: #eee + + .value + display: block + font-size: 18px + line-height: 24px + overflow: hidden + text-overflow: ellipsis + + label + display: none + + select + border: none + cursor: pointer + height: 50px + left: 0 + margin: 0 + opacity: 0 + position: absolute + top: 0 + z-index: 2 + width: 100% + + &:hover + background: #318ec4 + background: linear-gradient(#318ec4, #2b7cab) + border-color: #2e85b8 + color: #fff + + .on + background-image: none + background-color: rgba(255, 255, 255, .3) + border-color: transparent + + .icon-sm + color: #fff + + &:active + background: #2e85b8 + background: linear-gradient(#2e85b8, #28739f) + border-color: #2b7cab + color: #fff + + .button-link.negate + + &:hover + background: #990f0f + background: linear-gradient(#990f0f, #7d0c0c) + border-color: @background + + &:active + background: #7d0c0c + border-color: #990f0f + + + &.primary + background: #48b512 + background: linear-gradient(#48b512, #3d990f) + border: 1px solid + border-color: #3d990f + color: #fff + + &:hover + background: #3d990f + background: linear-gradient(#3d990f, #327d0c) + border-color: #3d990f + + &.danger + background: #ba1212 + background: linear-gradient(#ba1212, #8b0e0e) + border: 1px solid + border-color: #a21010 + color: #fff + + &:hover + background: #a21010 + background: linear-gradient(#a21010, #740b0b) + border-color: #8b0e0e + +button + + &.quiet-button, + &.loud-text-button + background: none + text-align: left + line-height: normal + border: none + box-shadow: none + + &:active + color: #4d4d4d + background: #d3d3d3 + box-shadow: none + + &.quiet-button + font-weight: 400 + text-decoration: underline + + &.loud-text-button + width: 100% + + &:hover + color: #111 + +.emphasis-button, +.quiet-button + border-radius: 3px + user-select: none + color: #8c8c8c + display: block + margin: 2px 0 + padding: 6px 8px + position: relative + + &.w-img + padding-left: 28px + + .icon-sm + left: 6px + position: absolute + top: 6px + + &:hover + color: #4d4d4d + background: #dcdcdc + + &:active + color: #4d4d4d + background: #d3d3d3 + +.quiet-button-large + padding: 16px 24px + +.emphasis-button + color: #74663e + background: #ecdfbb + + &:hover + color: #53492d + background: #e7d6a7 + + &:active + color: #53492d + background: #e1cc93 + +.big-link + border-radius: 3px + display: block + margin: 6px 0 6px 40px + padding: 11px + position: relative + text-decoration: none + font-size: 16px + line-height: 20px + + .text + text-decoration: underline + + &:hover + background: #dcdcdc + + &.options + padding-right: 41px + + .option + height: 30px + width: @height + position: absolute + right: 6px + top: 6px + + &.none + color: #8c8c8c + text-decoration: none + + &:hover + background: transparent + + &.avatar-changer + padding-right: 51px + + .member + border: 1px solid #ccc + border-radius: 3px + height: 40px + width: @height + position: absolute + right: 0 + top: 0 + + .member-avatar + height: 40px + width: @height + + .member-initials + font-size: 16px + height: 40px + line-height: @height + max-height: @height + +.show-more + border-radius: 3px + color: #8c8c8c + display: block + padding: 16px 8px 16px 40px + margin: 8px 0 + + &:hover + background: #dcdcdc + text-decoration: underline + + &.compact + padding: 12px 8px + margin: 8px 0 0 + text-align: center + +.board-widget .show-more + padding: 12px 8px 12px 40px + +.uploader + clear: both + cursor: pointer + position: relative + height: 34px + width: 100% + + .realfile + cursor: pointer + height: 34px + line-height: @height + position: absolute + top: 0 + left: 0 + width: 100% + z-index: 2 + font-size: 23px + + input[type="file"] + cursor: pointer + height: 34px + line-height: @height + margin: 0 + opacity: 0 + padding: 0 + width: 100% + z-index: 2 + font-size: 23px + + &:hover .fakefile + background: #318ec4 + background: linear-gradient(#318ec4, #2b7cab) + border-color: #2e85b8 + color: #fff + +.form-grid + display: flex + flex-wrap: wrap + width: 100% + +.form-grid-child + flex: 1 + margin: 0 0 8px + +.form-grid-child-full + flex: 1 1 100% + +.form-grid-child-threequarters + flex: 3 + margin-right: 8px + +.form-grid-child-twothirds + flex: 2 + margin-right: 8px + +.dropdown-menu + border-radius: 2px + // padding-bottom: 3px + overflow: hidden + + li + border-top: none + + a + padding: 4px 12px 4px 8px + + img + width: 18px + height: @width + margin-right: 5px + vertical-align: middle + + &.active + background: #005377 + + a + color: white diff --git a/client/components/forms/inlinedform.jade b/client/components/forms/inlinedform.jade new file mode 100644 index 00000000..5ad9039e --- /dev/null +++ b/client/components/forms/inlinedform.jade @@ -0,0 +1,6 @@ +template(name='inlinedForm') + if isOpen.get + form(id=id class=classNames) + +Template.contentBlock + else + +Template.elseBlock diff --git a/client/components/forms/inlinedform.js b/client/components/forms/inlinedform.js new file mode 100644 index 00000000..2e2b2eba --- /dev/null +++ b/client/components/forms/inlinedform.js @@ -0,0 +1,93 @@ +// A inlined form is used to provide a quick edition of single field for a given +// document. Clicking on a edit button should display the form to edit the field +// value. The form can then be submited, or just closed. +// +// When the form is closed we save non-submitted values in memory to avoid any +// data loss. +// +// Usage: +// +// +inlineForm +// // the content when the form is open +// else +// // the content when the form is close (optional) + +// We can only have one inlined form element opened at a time +// XXX Could we avoid using a global here ? This is used in Mousetrap +// keyboard.js +currentlyOpenedForm = new ReactiveVar(null); + +BlazeComponent.extendComponent({ + template: function() { + return 'inlinedForm'; + }, + + mixins: function() { + return [Mixins.CachedValue]; + }, + + onCreated: function() { + this.isOpen = new ReactiveVar(false); + }, + + open: function() { + // Close currently opened form, if any + if (currentlyOpenedForm.get() !== null) { + currentlyOpenedForm.get().close(); + } + this.isOpen.set(true); + currentlyOpenedForm.set(this); + }, + + close: function() { + this.saveValue(); + this.isOpen.set(false); + currentlyOpenedForm.set(null); + }, + + getValue: function() { + return this.isOpen.get() && this.find('textarea,input[type=text]').value; + }, + + saveValue: function() { + this.callFirstWith(this, 'setCache', this.getValue()); + }, + + events: function() { + return [{ + 'click .js-close-inlined-form': this.close, + 'click .js-open-inlined-form': this.open, + + // Close the inlined form by pressing escape. + // + // Keydown (and not keypress) in necessary here because the `keyCode` + // property is consistent in all browsers, (there is not keyCode for the + // `keypress` event in firefox) + 'keydown form input, keydown form textarea': function(evt) { + if (evt.keyCode === 27) { + evt.preventDefault(); + this.close(); + } + }, + + // Pressing Ctrl+Enter should submit the form + 'keydown form textarea': function(evt) { + if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) { + $(evt.currentTarget).parents('form:first').submit(); + } + }, + + // Close the inlined form when after its submission + submit: function() { + var self = this; + // XXX Swith to an arrow function here when we'll have ES6 + if (this.currentData().autoclose !== false) { + Tracker.afterFlush(function() { + self.close(); + self.callFirstWith(self, 'resetCache'); + }); + } + } + }]; + } +}).register('inlinedForm'); 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(); + } +}); diff --git a/client/components/main/events.js b/client/components/main/events.js new file mode 100644 index 00000000..beb90c5e --- /dev/null +++ b/client/components/main/events.js @@ -0,0 +1,8 @@ +Template.editor.events({ + // Pressing Ctrl+Enter should submit the form. + 'keydown textarea': function(event) { + if (event.keyCode === 13 && (event.metaKey || event.ctrlKey)) { + $(event.currentTarget).parents('form:first').submit(); + } + } +}); diff --git a/client/components/main/header.jade b/client/components/main/header.jade new file mode 100644 index 00000000..588c9b6e --- /dev/null +++ b/client/components/main/header.jade @@ -0,0 +1,40 @@ +template(name="header") + #header(class=currentBoard.colorClass) + //- + If the user is connected we display a small "quick-access" top bar that + list all starred boards with a link to go there. This is inspired by the + Reddit "subreddit" bar. + The first link goes to the boards page. + if currentUser + #header-quick-access + ul + li + +linkTo(route="Boards") + span.fa.fa-home + | All boards + each currentUser.starredBoards + li.separator - + li(class="{{#if $.Session.equals 'currentBoard' _id}}current{{/if}}") + +linkTo(route="Board" data=this) + = title + else + li.current Star a board to add a shortcut in this bar. + + li + a.js-create-board + i.fa.fa-plus(title="Create a new board") + + +headerUserBar + + //- + The main bar is a colorful bar that provide all the meta-data for the + current page. This bar is contextual based. + If the user is not connected we display "sign in" and "log in" buttons. + #header-main-bar + if $.Session.get 'currentBoard' + +headerBoard + else + +headerTitle + +template(name="headerTitle") + h1 LibreBoard diff --git a/client/components/main/header.js b/client/components/main/header.js new file mode 100644 index 00000000..2a545309 --- /dev/null +++ b/client/components/main/header.js @@ -0,0 +1,10 @@ +Template.header.helpers({ + // Reactively set the color of the page from the color of the current board. + headerTemplate: function() { + return 'headerBoard'; + } +}); + +Template.header.events({ + 'click .js-create-board': Popup.open('createBoard') +}); diff --git a/client/components/main/header.styl b/client/components/main/header.styl new file mode 100644 index 00000000..1177d930 --- /dev/null +++ b/client/components/main/header.styl @@ -0,0 +1,266 @@ +@import 'nib' + +global-reset() + +#header + color: white + transition: background-color 0.4s + background: #27AE60 + + #header-quick-access + background-color: rgba(0, 0, 0, 0.2) + padding: 4px 10px + height: 16px + font-size: 12px + display: flex + + ul li, #header-user-bar + color: darken(white, 17%) + + a + color: inherit + text-decoration: none + + &:hover + color: white + + ul + flex: 1 + transition: opacity 0.2s + margin-left: 5px + + li + display: block + float: left + width: auto + color: darken(white, 15%) + padding: 0 4px 1px 4px + + &.separator + padding: 0 2px 1px 2px + + &.current + font-style: italic + + &:first-child .fa-home + margin-right: 5px + + #header-main-bar + height: 30px + padding: 8px + + h1 + font-size: 19px + line-height: 1.7em + margin: 0 20px 0 10px + float: left + + &.header-board-menu + cursor: pointer + + .fa-angle-down + font-size: 0.8em + // line-height: 1.1em + margin-left: 5px + + .board-header-starred .fa + color: yellow + + .board-header-members + float: right + + .member + display: block + width: 32px + height: @width + + .add-board-member + color: white + display: flex + align-items: center + justify-content: center + border: 1px solid white + height: 32px - 2px + width: @height + + i.fa-plus + margin-top: 2px + + .header-btn:last-child + margin-right: 0 + + + +// #header { +// background: #138871; +// height: 30px; +// overflow: hidden; +// padding: 5px; +// position: relative; +// z-index: 10; +// } + +// .header-logo { +// bottom: 0; +// display: block; +// height: 25px; +// left: 50%; +// position: absolute; +// top: 8px; +// width: 80px; +// margin-left: - @width/2; +// text-align: center; +// z-index: 2; +// opacity: .5; +// transition: opacity ease-in 85ms; +// color: white; +// font-size: 22px; +// text-decoration: none; +// background-image: url('/logos/white_logo.png'); + +// &:hover { +// opacity: .8; +// color: white; +// } +// } + +// .header-btn.header-btn-feedback { +// background: rgba(255, 255, 255, .1); +// background: linear-gradient(to bottom, rgba(255, 255, 255, .1) 0, rgba(255, 255, 255, .05) 100%); +// padding-left: 22px; +// margin-right: 16px; + +// .header-btn-icon { +// top: 1px; +// } +// } + +.header-btn { + border-radius: 3px; + user-select: none; + background: rgba(255, 255, 255, .3); + background: linear-gradient(to bottom, rgba(255, 255, 255, .3) 0, rgba(255, 255, 255, .2) 100%); + color: #f3f3f3; + display: block; + float: left; + font-weight: 700; + height: 30px; + line-height: 30px; + padding: 0 10px; + position: relative; + margin-right: 8px; + min-width: 30px; + text-decoration: none; + cursor: pointer; + + .header-btn-icon { + font-size: 16px; + line-height: 28px; + position: absolute; + top: 0; + left: 0; + } + + &.new-notifications { + background: #ba1212; + + &:hover { + background: #d11515; + } + } + + &.header-member .member { + margin: 0; + border-top-left-radius: 3px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 3px; + + &:hover .member-avatar { + opacity: 1; + } + } + + &:hover { + background: rgba(255, 255, 255, .4); + background: linear-gradient(to bottom, rgba(255, 255, 255, .4) 0, rgba(255, 255, 255, .3) 100%); + color: #fff; + + .header-btn-count { + background: #d11515; + } + } + + &:active { + background: rgba(255, 255, 255, .4); + background: linear-gradient(to bottom, rgba(255, 255, 255, .4) 0, rgba(255, 255, 255, .3) 100%); + } + + &.upgrade { + margin-right: 16px; + + .icon-sm { + padding: 6px 2px 6px 4px; + } + } + + &.upgrade, + &.header-boards { + padding-left: 4px; + } + + &.header-boards { + padding-right: 4px; + } + + &.header-login, + &.header-signup { + padding: 0 12px; + } + + &.header-signup { + background: #48b512; + background: linear-gradient(to bottom, #48b512 0, #3d990f 100%); + + &:hover { + background: #3d990f; + background: linear-gradient(to bottom, #3d990f 0, #327d0c 100%); + } + + &:active { + background: #327d0c; + } + } + + &.header-go-to-boards { + padding: 0 8px 0 38px; + } + + &.header-go-to-boards .member { + border-top-left-radius: 3px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 3px; + position: absolute; + left: 0; + } +} + +// .header-btn-text { +// padding: 0 8px; +// } + +// .header-notification-list ul { +// margin-top: 8px; +// } + +// .header-notification-list .action-comment { +// max-height: 250px; +// overflow-y: auto; +// } + +// .header-user { +// position: absolute; +// top: 5px; +// right: 0; +// } diff --git a/client/components/main/helpers.js b/client/components/main/helpers.js new file mode 100644 index 00000000..7ad602f1 --- /dev/null +++ b/client/components/main/helpers.js @@ -0,0 +1,63 @@ +var Helpers = { + error: function() { + return Session.get('error'); + }, + + toLowerCase: function(text) { + return text && text.toLowerCase(); + }, + + toUpperCase: function(text) { + return text && text.toUpperCase(); + }, + + firstChar: function(text) { + return text && text[0].toUpperCase(); + }, + + session: function(prop) { + return Session.get(prop); + }, + + getUser: function(userId) { + return Users.findOne(userId); + } +}; + +// Register all Helpers +_.each(Helpers, function(fn, name) { Blaze.registerHelper(name, fn); }); + +// XXX I believe we should compute a HTML rendered field on the server that +// would handle markdown, emojies and user mentions. We can simply have two +// fields, one source, and one compiled version (in HTML) and send only the +// compiled version to most users -- who don't need to edit. +// In the meantime, all the transformation are done on the client using the +// Blaze API. +var at = HTML.CharRef({html: '@', str: '@'}); +Blaze.Template.registerHelper('mentions', new Template('mentions', function() { + var view = this; + var content = Blaze.toHTML(view.templateContentBlock); + var currentBoard = Session.get('currentBoard'); + var knowedUsers = _.map(currentBoard.members, function(member) { + member.username = Users.findOne(member.userId).username; + return member; + }); + + var mentionRegex = /\B@(\w*)/gi; + var currentMention, knowedUser, href, linkClass, linkValue, link; + while (currentMention = mentionRegex.exec(content)) { + + knowedUser = _.findWhere(knowedUsers, { username: currentMention[1] }); + if (! knowedUser) + continue; + + linkValue = [' ', at, knowedUser.username]; + href = Router.url('Profile', { username: knowedUser.username }); + linkClass = 'atMention' + (knowedUser.userId === Meteor.userId() ? ' me' : ''); + link = HTML.A({ href: href, 'class': linkClass }, linkValue); + + content = content.replace(currentMention[0], Blaze.toHTML(link)); + } + + return HTML.Raw(content); +})); diff --git a/client/components/main/layouts.jade b/client/components/main/layouts.jade new file mode 100644 index 00000000..18df4d9e --- /dev/null +++ b/client/components/main/layouts.jade @@ -0,0 +1,17 @@ +head + title LibreBoard + meta(name="viewport" + content="maximum-scale=1.0,width=device-width,initial-scale=1.0,user-scalable=0") + link(rel="shortcut icon" href="/favicon.png") + +template(name="userFormsLayout") + h1.at-form-landing-logo + img(src="/logo.png" title="LibreBoard") + +yield + +template(name="defaultLayout") + #surface + +header + #content + +yield + diff --git a/client/components/main/popup.js b/client/components/main/popup.js new file mode 100644 index 00000000..53695d10 --- /dev/null +++ b/client/components/main/popup.js @@ -0,0 +1,16 @@ +Popup.template.events({ + click: function(evt) { + if (evt.originalEvent) { + evt.originalEvent.clickInPopup = true; + } + }, + 'click .js-back-view': function() { + Popup.back(); + }, + 'click .js-close-popover': function() { + Popup.close(); + }, + 'click .js-confirm': function() { + this.__afterConfirmAction.call(this); + } +}); diff --git a/client/components/main/popup.styl b/client/components/main/popup.styl new file mode 100644 index 00000000..8c9993af --- /dev/null +++ b/client/components/main/popup.styl @@ -0,0 +1,585 @@ +@import 'nib' + +.pop-over + background: #fff + border-radius: 3px + border: 1px solid #dbdbdb + border-bottom-color: #c2c2c2 + box-shadow: 0 1px 6px rgba(0, 0, 0, .3) + display: none + overflow: hidden + position: absolute + width: 300px + z-index: 99999 + margin-top: 5px + + hr + margin: 4px -10px + width: 275px + 2*10px + + input[type="text"], + input[type="email"], + input[type="password"] + margin: 4px 0 12px + width: 100% + + input[type="file"] + width: 240px + + select + width: 100% + margin-bottom: 14px + + textarea + height: 72px + margin: 4px 0 12px + width: 100% + + .empty + margin: 0 + + img + max-width: 270px + + .custom-image img + height: 18px + left: 9px + top: 9px + width: 18px + + .title + line-height: 32px + + .header + height: 36px + position: relative + margin-bottom: 8px + background: #F7F7F7 + border-bottom: 1px solid #dcdcdc + color: darken(white, 60%) + + .header-title + display: block + line-height: 32px + padding-top: 4px + margin: 0 10px + font-weight: bold + overflow: hidden + text-overflow: ellipsis + white-space: nowrap + + .back-btn, .close-btn + &:hover .icon-sm + color: darken(white, 80%) + + .back-btn + padding: 10px + float: left + + .close-btn + padding: 10px 10px 10px 4px + position: absolute + top: 0 + right: 0 + + .content + overflow-x: hidden + overflow-y: auto + padding: 0 10px 10px + max-height: 550px + + .quiet + padding: 6px 6px 4px + + &.search-over + background: #f0f0f0 + min-height: 114px + + .header + display: none + + .content + padding: 8px 4px 8px 10px + margin-right: 8px + + &::-webkit-scrollbar-button + display: block + height: 4px + width: 4px + +.select-members-list + margin-bottom: 8px + +.pop-over-list + + &.navigable li.not-selectable>a:hover, + li.not-selectable>a:hover + color: #8c8c8c + cursor: default + + .icon-sm + color: #a6a6a6 + + li > a + cursor: pointer + display: block + font-weight: 700 + padding: 6px 10px + position: relative + margin: 0 -10px + text-decoration: none + + .item-name + display: block + width: auto + padding-right: 22px + + &:hover + background-color: #005377 + color: #fff + + .sub-name, + .quiet + color: #eee + + .unread-indicator + background: #fff + + .icon-sm + color: #fff + + .sub-name + clear: both + color: #8c8c8c + display: block + font-size: 12px + font-weight: 400 + line-height: 15px + margin-top: 4px + + &.current + background-color: #e2e6e9 + + .unread-indicator + background: #2e85b8 + background: linear-gradient(to bottom, #2e85b8 0, #2b7cab 100%) + border-radius: 7px + display: block + height: 14px + opacity: 0 + position: absolute + right: 16px + top: 8px + width: 14px + + &.any + opacity: 1 + + &:active + background-color: #2e85b8 + + &.disabled + color: #8c8c8c + cursor: default + + .vis-icon + opacity: .35 + + .icon-sm + color: #a6a6a6 + + &:hover + background: none + + .sub-name, + .quiet + color: #8c8c8c + + .icon-sm + color: #a6a6a6 + + &:active + background: none + + &.inset li > a + border-radius: 3px + margin: 0 + + .pop-over-list.checkable + + .icon-check + display: none + position: absolute + top: 6px + right: 12px + + li.active a + padding-right: 28px + + .icon-check + display: block + + &.left-check + + .icon-check + right: auto + left: 10px + + li a + padding-right: 10px + padding-left: 30px + + li.active a + padding-right: 10px + + &.normal-weight li>a + font-weight: 400 + + &.navigable + + li > a:hover + background-color: transparent + color: #4d4d4d + + .sub-name, + .quiet + color: #8c8c8c + + .icon-sm + color: #a6a6a6 + + li.selected > a + background-color: #005377 + color: #fff + + .sub-name, + .quiet + color: #eee + + li.selected > a + + &.current + background-color: #005377 + + .unread-indicator + background: #fff + + .icon-sm + color: #fff + + &:active + background-color: #005377 + +.pop-over.miniprofile + + .header + border-bottom-color: transparent + height: 30px + position: absolute + right: 0 + top: 0 + width: 60px + z-index: 1 + + .header-title + display: none + + .pop-over-list + padding-top: 8px + +.mini-profile-info + margin-top: 8px + min-height: 56px + position: relative + + .member-large + position: absolute + top: 2px + left: 2px + + .info + margin: 0 0 0 64px + word-wrap: break-word + + h3 a + text-decoration: none + + &:hover + text-decoration: underline + +.pop-over.avdetail .header + border-bottom-color: transparent + height: 20px + position: absolute + top: 8px + left: 8px + right: 8px + z-index: 0 + +.pop-over.avdetail .header-title + display: none + +.pop-over.avdetail .content + text-align: center + +.pop-over.avdetail .mem-info + margin: 2px 24px 8px + position: relative + z-index: 1 + width: 222px + +.pop-over.avdetail .mem-info h3 a + text-decoration: none + +.pop-over.avdetail .mem-info h3 a:hover + text-decoration: underline + +.pop-over-label-list li, +.pop-over-member-list li + + &.disabled a + cursor:default + + &:not(.disabled):hover a + background-color: #005377 + color: #fff + + +.pop-over-label-list, +.pop-over-member-list, +.pop-over-emoji-list, +.pop-over-card-list + li + a + border-radius: 3px + display: block + height: 30px + line-height: 30px + overflow: hidden + position: relative + text-overflow: ellipsis + text-decoration: none + white-space: nowrap + padding: 4px + margin-bottom: 2px + + &.multi-line + line-height: 16px + + .member + margin-right: 8px + + .card-label + float: left + height: 30px + margin: 0 8px 0 0 + padding: 0 + width: 30px + + .option, + .icon-check + background-clip: content-box + background-origin: content-box + padding: 11px + position: absolute + top: 0 + right: 0 + + .sub-name + font-size: 12px + + + &:last-child a + margin-bottom: 0 + + &.disabled + opacity: .5 + + &.active a, + &.selected a + background: none + color: #4d4d4d + cursor: default + + .quiet + color: #8c8c8c + + &.email-invite + + .member + display: none + + a + padding: 0 10px + + &.selected a + background-color: #005377 + color: #fff + + .quiet + color: #eee + + .card-label + border-radius: 3px + + .icon-check + color: #fff + + &.active a .icon-check + display: block + + &.unconfirmed a.name + line-height: 16px + + &.options li + + &.selected a + padding-right: 28px + + .option + display: block + opacity: .5 + + &:hover + opacity: 1 + + &.disabled.selected a + padding-right: 0 + + .option + display: none + + + &.no-option.selected a + padding-right: 6px + + .option + display: none + + &.collapsed + + &.checkable li.active a + padding-right: 0 + + li + float: left + margin: 0 3px 3px 0 + + a + padding: 0 + margin: 0 + width: 30px + + .member + opacity: .8 + + .full-name + display: none + + &.selected a .member, + &.active.selected a .member + border-color: #005377 + opacity: .9 + + &.active a + + .member + border-color: #2e85b8 + opacity: 1 + + .icon-check + border-radius: 3px + background-color: #2e85b8 + bottom: 0 + color: #fff + display: block + padding: 0 + right: 0 + top: auto + + &.checkable li.active a + padding-right: 28px + + &.filtered li + display: none + + &.matches-filter + display: block + + &.limited li.exceeds-limit + display: none + +.pop-over-emoji-list li > a + padding: 2px 4px + + .emoji + margin: 0 6px + +.pop-over-card-list li > a + padding: 2px 4px + +.login-signup-popover + padding: 15px + + .form-tabs + display: none + + h1 + margin-bottom: 15px + + p + margin: 8px 0 + + .form-parts-container + position: relative + + .active-box + position: absolute + top: 0 + background: #e2e2e2 + border: 1px solid #c9c9c9 + border-radius: 3px + z-index: 1 + height: 100% + width: 49% + transition-property: all + transition-duration: .4s + opacity: 1 + + &.start + opacity: 0 + left: 25% + + .signup-form, + .login-form + position: relative + box-sizing: border-box + padding: 20px + width: 50% + z-index: 2 + opacity: .3 + transition-property: opacity + transition-duration: .2s + + .active + opacity: 1 + + + .js-signup-form-pos + left: 0 + + .login-form + position: absolute + top: 0 + + .login-form .icon-google + position: absolute + left: 5px + top: 3px + + .login-form .button.google + padding-left: 40px + margin: 0 0 15px 0 + + .js-login-form-pos + left: 50% diff --git a/client/components/main/popup.tpl.jade b/client/components/main/popup.tpl.jade new file mode 100644 index 00000000..ba24db0a --- /dev/null +++ b/client/components/main/popup.tpl.jade @@ -0,0 +1,13 @@ +.pop-over.clearfix( + class="{{#unless title}}miniprofile{{/unless}}" + class=currentBoard.colorClass + style="display:block; left:{{offset.left}}px; top:{{offset.top}}px;") + .header.clearfix + if hasPopupParent + a.back-btn.js-back-view + i.fa.fa-chevron-left + span.header-title= title + a.close-btn.js-close-popover + i.fa.fa-times + .content.clearfix + +Template.dynamic(template=popupName data=dataContext) diff --git a/client/components/main/rendered.js b/client/components/main/rendered.js new file mode 100644 index 00000000..787e8225 --- /dev/null +++ b/client/components/main/rendered.js @@ -0,0 +1,40 @@ +Template.editor.rendered = function() { + this.$('textarea').textcomplete([ + // Emojies + { + match: /\B:([\-+\w]*)$/, + search: function(term, callback) { + callback($.map(Emoji.values, function(emoji) { + return emoji.indexOf(term) === 0 ? emoji : null; + })); + }, + template: function(value) { + var image = '<img src="' + Emoji.baseImagePath + value + '.png"></img>'; + return image + value; + }, + replace: function(value) { + return ':' + value + ':'; + }, + index: 1 + }, + + // User mentions + { + match: /\B@(\w*)$/, + search: function(term, callback) { + var currentBoard = Boards.findOne(Session.get('currentBoard')); + callback($.map(currentBoard.members, function(member) { + var username = Users.findOne(member.userId).username; + return username.indexOf(term) === 0 ? username : null; + })); + }, + template: function(value) { + return value; + }, + replace: function(username) { + return '@' + username + ' '; + }, + index: 1 + } + ]); +}; diff --git a/client/components/main/router.js b/client/components/main/router.js new file mode 100644 index 00000000..bae832e8 --- /dev/null +++ b/client/components/main/router.js @@ -0,0 +1,5 @@ +Router.route('/', { + name: 'Home', + redirectLoggedInUsers: true, + authenticated: true +}); diff --git a/client/components/main/spinner.styl b/client/components/main/spinner.styl new file mode 100644 index 00000000..f4b8cc86 --- /dev/null +++ b/client/components/main/spinner.styl @@ -0,0 +1,45 @@ +/* + * From https://github.com/tobiasahlin/SpinKit + * + * Usage: + * + * <div class="sk-spinner sk-spinner-wave"> + * <div class="sk-rect1"></div> + * <div class="sk-rect2"></div> + * <div class="sk-rect3"></div> + * <div class="sk-rect4"></div> + * <div class="sk-rect5"></div> + * </div> + * + */ + +.sk-spinner-wave { + + &.sk-spinner { + width: 50px; + height: 50px; + margin: auto; + margin-top: 30vh; + text-align: center; + font-size: 10px; + } + + div { + background-color: #333; + height: 100%; + width: 6px; + display: inline-block; + + animation: sk-waveStretchDelay 1.2s infinite ease-in-out; + } + + .sk-rect2 { animation-delay: -1.1s } + .sk-rect3 { animation-delay: -1.0s } + .sk-rect4 { animation-delay: -0.9s } + .sk-rect5 { animation-delay: -0.8s } +} + +@keyframes sk-waveStretchDelay { + 0%, 40%, 100% { transform: scaleY(0.4) } + 20% { transform: scaleY(1.0) } +} diff --git a/client/components/main/spinner.tpl.jade b/client/components/main/spinner.tpl.jade new file mode 100644 index 00000000..9310a6e5 --- /dev/null +++ b/client/components/main/spinner.tpl.jade @@ -0,0 +1,6 @@ +.sk-spinner.sk-spinner-wave(class=currentBoard.colorClass) + .sk-rect1 + .sk-rect2 + .sk-rect3 + .sk-rect4 + .sk-rect5 diff --git a/client/components/main/templates.html b/client/components/main/templates.html new file mode 100644 index 00000000..e9be0f93 --- /dev/null +++ b/client/components/main/templates.html @@ -0,0 +1,18 @@ +<template name="notfound"> + {{ > message label='page-not-found'}} +</template> + +<template name='message'> + <div class="big-message quiet {{ color }}"> + <h1>{{_ label}}</h1> + {{#with pathFor route='Login'}} + <p>{{{_ 'page-maybe-private' this}}}</p> + {{/with}} + </div> +</template> + +<template name="editor"> + <textarea class="{{class}}" placeholder="{{_ 'comment-placeholder'}}" id="{{id}}" tabindex="1">{{> UI.contentBlock }}</textarea> +</template> + +<template name="viewer">{{#markdown}}{{#emoji}}{{#mentions}}{{> UI.contentBlock }}{{/mentions}}{{/emoji}}{{/markdown}}</template> diff --git a/client/components/modal/events.js b/client/components/modal/events.js new file mode 100644 index 00000000..2943f841 --- /dev/null +++ b/client/components/modal/events.js @@ -0,0 +1,14 @@ +Template.modal.events({ + 'click .window-overlay': function(event) { + // We only want to catch the event if the user click on the .window-overlay + // div itself, not a child (ie, not the overlay window) + if (event.target !== event.currentTarget) + return; + Utils.goBoardId(this.card.board()._id); + event.preventDefault(); + }, + 'click .js-close-window': function(event) { + Utils.goBoardId(this.card.board()._id); + event.preventDefault(); + } +}); diff --git a/client/components/modal/helpers.js b/client/components/modal/helpers.js new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/client/components/modal/helpers.js diff --git a/client/components/modal/modal.tpl.jade b/client/components/modal/modal.tpl.jade new file mode 100644 index 00000000..2f40ad75 --- /dev/null +++ b/client/components/modal/modal.tpl.jade @@ -0,0 +1,5 @@ +.window-overlay.show + .window + .window-wrapper.clearfix + a.icon-lg.fa.fa-times.dialog-close-button.js-close-window(title="{{_ 'modal-close-title'}}") + +UI.dynamic(template=template) diff --git a/client/components/sidebar/events.js b/client/components/sidebar/events.js new file mode 100644 index 00000000..1067421f --- /dev/null +++ b/client/components/sidebar/events.js @@ -0,0 +1,93 @@ +Template.filterSidebar.events({ + 'click .js-toggle-label-filter': function(event) { + Filter.labelIds.toogle(this._id); + Filter.resetExceptions(); + event.preventDefault(); + }, + 'click .js-toogle-member-filter': function(event) { + Filter.members.toogle(this._id); + Filter.resetExceptions(); + event.preventDefault(); + }, + 'click .js-clear-all': function(event) { + Filter.reset(); + event.preventDefault(); + } +}); + +var getMemberIndex = function(board, searchId) { + for (var i = 0; i < board.members.length; i++) { + if (board.members[i].userId === searchId) + return i; + } + throw new Meteor.Error('Member not found'); +}; + +Template.memberPopup.events({ + 'click .js-filter-member': function() { + Filter.members.toogle(this.userId); + Popup.close(); + }, + 'click .js-change-role': Popup.open('changePermissions'), + 'click .js-remove-member': Popup.afterConfirm('removeMember', function() { + var currentBoard = Boards.findOne(Session.get('currentBoard')); + var memberIndex = getMemberIndex(currentBoard, this.userId); + var setQuery = {}; + setQuery[['members', memberIndex, 'isActive'].join('.')] = false; + Boards.update(currentBoard._id, { $set: setQuery }); + Popup.close(); + }), + 'click .js-leave-member': function() { + // @TODO + Popup.close(); + } +}); + +Template.membersWidget.events({ + 'click .js-open-manage-board-members': Popup.open('addMember'), + 'click .member': Popup.open('member') +}); + +Template.labelsWidget.events({ + 'click .js-label': Popup.open('editLabel'), + 'click .js-add-label': Popup.open('createLabel') +}); + +// Template.addMemberPopup.events({ +// 'click .pop-over-member-list li:not(.disabled)': function(event, t) { +// var userId = this._id; +// var boardId = t.data.board._id; +// var currentMembersIds = _.pluck(t.data.board.members, 'userId'); +// if (currentMembersIds.indexOf(userId) === -1) { +// Boards.update(boardId, { +// $push: { +// members: { +// userId: userId, +// isAdmin: false, +// isActive: true +// } +// } +// }); +// } else { +// var memberIndex = getMemberIndex(t.data.board, userId); +// var setQuery = {}; +// setQuery[['members', memberIndex, 'isActive'].join('.')] = true; +// Boards.update(boardId, { $set: setQuery }); +// } +// Popup.close(); +// } +// }); + +// Template.changePermissionsPopup.events({ +// 'click .js-set-admin, click .js-set-normal': function(event) { +// var currentBoard = Boards.findOne(Session.get('currentBoard')); +// var memberIndex = getMemberIndex(currentBoard, this.user._id); +// var isAdmin = $(event.currentTarget).hasClass('js-set-admin'); +// var setQuery = {}; +// setQuery[['members', memberIndex, 'isAdmin'].join('.')] = isAdmin; +// Boards.update(currentBoard._id, { +// $set: setQuery +// }); +// Popup.back(1); +// } +// }); diff --git a/client/components/sidebar/helpers.js b/client/components/sidebar/helpers.js new file mode 100644 index 00000000..a76dad7f --- /dev/null +++ b/client/components/sidebar/helpers.js @@ -0,0 +1,51 @@ +var widgetTitles = { + filter: 'filter-cards', + background: 'change-background' +}; + +Template.boardSidebar.helpers({ + currentWidget: function() { + return Session.get('currentWidget') + 'Sidebar'; + }, + currentWidgetTitle: function() { + return TAPi18n.__(widgetTitles[Session.get('currentWidget')]); + } +}); + +// Template.addMemberPopup.helpers({ +// isBoardMember: function() { +// var user = Users.findOne(this._id); +// return user && user.isBoardMember(); +// } +// }); + +Template.memberPopup.helpers({ + user: function() { + return Users.findOne(this.userId); + }, + memberType: function() { + var type = Users.findOne(this.userId).isBoardAdmin() ? 'admin' : 'normal'; + return TAPi18n.__(type).toLowerCase(); + } +}); + +// Template.removeMemberPopup.helpers({ +// user: function() { +// return Users.findOne(this.userId) +// }, +// board: function() { +// return currentBoard(); +// } +// }); + +// Template.changePermissionsPopup.helpers({ +// isAdmin: function() { +// return this.user.isBoardAdmin(); +// }, +// isLastAdmin: function() { +// if (! this.user.isBoardAdmin()) +// return false; +// var nbAdmins = _.where(currentBoard().members, { isAdmin: true }).length; +// return nbAdmins === 1; +// } +// }); diff --git a/client/components/sidebar/infiniteScrolling.js b/client/components/sidebar/infiniteScrolling.js new file mode 100644 index 00000000..df3b8901 --- /dev/null +++ b/client/components/sidebar/infiniteScrolling.js @@ -0,0 +1,37 @@ +var peakAnticipation = 200; + +Mixins.InfiniteScrolling = BlazeComponent.extendComponent({ + onCreated: function() { + this._nextPeak = Infinity; + }, + + setNextPeak: function(v) { + this._nextPeak = v; + }, + + getNextPeak: function() { + return this._nextPeak; + }, + + resetNextPeak: function() { + this._nextPeak = Infinity; + }, + + // To be overwritten by consumers of this mixin + reachNextPeak: function() { + + }, + + events: function() { + return [{ + scroll: function(evt) { + var domElement = evt.currentTarget; + var altitude = domElement.scrollTop + domElement.offsetHeight; + altitude += peakAnticipation; + if (altitude >= this.callFirstWith(null, 'getNextPeak')) { + this.callFirstWith(null, 'reachNextPeak'); + } + } + }]; + } +}); diff --git a/client/components/sidebar/rendered.js b/client/components/sidebar/rendered.js new file mode 100644 index 00000000..2b58bf33 --- /dev/null +++ b/client/components/sidebar/rendered.js @@ -0,0 +1,21 @@ +Template.membersWidget.rendered = function() { + if (! Meteor.user().isBoardMember()) + return; + + _.each(['.js-member', '.js-label'], function(className) { + Utils.liveEvent('mouseover', function($this) { + $this.find(className).draggable({ + appendTo: 'body', + helper: 'clone', + revert: 'invalid', + revertDuration: 150, + snap: false, + snapMode: 'both', + start: function() { + Popup.close(); + } + }); + }); + }); +}; + diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js new file mode 100644 index 00000000..3f0142d4 --- /dev/null +++ b/client/components/sidebar/sidebar.js @@ -0,0 +1,55 @@ +BlazeComponent.extendComponent({ + template: function() { + return 'boardSidebar'; + }, + + mixins: function() { + return [Mixins.InfiniteScrolling]; + }, + + onCreated: function() { + this._isOpen = new ReactiveVar(true); + }, + + isOpen: function() { + return this._isOpen.get(); + }, + + open: function() { + if (! this._isOpen.get()) { + this._isOpen.set(true); + } + }, + + hide: function() { + if (this._isOpen.get()) { + this._isOpen.set(false); + } + }, + + toogle: function() { + this._isOpen.set(! this._isOpen.get()); + }, + + calculateNextPeak: function() { + var altitude = this.find('.js-board-sidebar-content').scrollHeight; + this.callFirstWith(this, 'setNextPeak', altitude); + }, + + reachNextPeak: function() { + var activitiesComponent = this.componentChildren('activities')[0]; + activitiesComponent.loadNextPage(); + }, + + isTongueHidden: function() { + return this.isOpen() && Filter.isActive(); + }, + + events: function() { + // XXX Hacky, we need some kind of `super` + var mixinEvents = this.getMixin(Mixins.InfiniteScrolling).events(); + return mixinEvents.concat([{ + 'click .js-toogle-sidebar': this.toogle + }]); + } +}).register('boardSidebar'); diff --git a/client/components/sidebar/sidebar.styl b/client/components/sidebar/sidebar.styl new file mode 100644 index 00000000..4b741dc7 --- /dev/null +++ b/client/components/sidebar/sidebar.styl @@ -0,0 +1,154 @@ +@import 'nib' + +.sidebar + .sidebar-content + padding: 10px 20px + background: white + box-shadow: -10px 0px 5px -10px darken(white, 30%) + z-index: 10 + position: absolute + top: 0 + bottom: 0 + right: 0 + left: 0 + overflow-x: hidden + overflow-y: auto + + h3 + color: darken(white, 50%) + + hr + margin: 8px 0 + +.board-sidebar + width: 248px + position: absolute + top: 0 + right: -@width + bottom: 0 + transition: top .1s, right .1s, width .1s + + &.is-open + right: 0 + +.board-widget-nav + border-radius: 3px + background: #dcdcdc + overflow: hidden + padding: 0 + position: relative + + .toggle-widget-nav + border-radius: 3px + color: #8c8c8c + margin: 0 + padding: 7px 10px + position: relative + cursor: pointer + + .toggle-menu-icon + position: absolute + top: 8px + right: 8px + + &:hover + background: #ccc + color: #4d4d4d + + .nav-list + display: block + opacity: 1 + max-height: 400px + + hr + margin: 2px 0 + color: #ccc + background: #ccc + + .nav-list-item + display: block + font-weight: 700 + line-height: 30px + overflow: hidden + padding: 0 8px 0 36px + position: relative + text-decoration: none + + .icon-type + left: 10px + position: absolute + top: 6px + + &:hover + background: #ccc + + .icon-type + color: #686868 + + .nav-list-sub-item + font-weight: 400 + color: #666 + + &:hover + color: #4d4d4d + + &.collapsed + + .nav-list + max-height: 0 + opacity: 0 + + hr + margin: 0 + + .toggle-widget-nav + color: #4d4d4d + + +.board-widget-title + display: block + min-height: 20px + margin-bottom: 6px + +.board-widget-content + position: relative + z-index: 1 + +.board-widget h4 + margin: 5px 0 + +.board-widget-activity + margin-right: -4px + +.sidebar-tongue + display: block + width: 30px + height: @width + left: -@width + position: absolute + top: 12px + z-index: 15 + background: white + border-radius: left 3px + box-shadow: -4px 0px 7px -4px darken(white, 30%) + color: darken(white, 50%) + transition: left .1s + + i.fa + margin: 9px + transition: transform 0.5s + + .board-sidebar.is-open & + left: -@width + 2px + + // XXX Bug: we should add a padding left + &:hover + left: -@width + 5px + + i.fa + transform: rotate(180deg) + + &.is-hidden, + .board-sidebar.is-open &.is-hidden + z-index: 0 + left: 5px diff --git a/client/components/sidebar/templates.html.old b/client/components/sidebar/templates.html.old new file mode 100644 index 00000000..d8b063f0 --- /dev/null +++ b/client/components/sidebar/templates.html.old @@ -0,0 +1,307 @@ +<template name="boardWidgets"> + <a href="#" class="sidebar-show-btn dark-hover js-show-sidebar"> + <span class="icon-sm fa fa-chevron-left"></span> + <span class="text">{{_ 'show-sidebar'}}</span> + </a> + <div class="board-widgets {{#if session 'sidebarIsOpen'}}show{{else}}hide{{/if}}"> + <div> + <a href="#" class="sidebar-hide-btn dark-hover js-hide-sidebar" title="{{_ 'close-sidebar-title'}}"> + <span class="icon-sm fa fa-chevron-right"></span> + </a> + {{#unless isTrue currentWidget "homeWidget"}} + <div class="board-widgets-title clearfix"> + <a href="#" class="board-sidebar-back-btn js-pop-widget-view"> + <span class="left-arrow"></span>{{_ 'back'}} + </a> + <h3 class="text">{{currentWidgetTitle}}</h3> + <hr> + </div> + {{/unless}} + <div class="board-widgets-content-wrapper"> + <div class="board-widgets-content default fancy-scrollbar short{{#unless session 'menuWidgetIsOpen'}} short{{/unless}}"> + {{> UI.dynamic template=currentWidget data=this }} + </div> + </div> + </div> + </div> +</template> + +<template name="homeWidget"> +{{ > menuWidget }} +{{ > membersWidget }} +{{ > activityWidget }} +</template> + +<template name="menuWidget"> + <div class="board-widget board-widget-nav clearfix{{#unless session 'menuWidgetIsOpen'}} collapsed{{/unless}}"> + <h3 class="dark-hover toggle-widget-nav js-toggle-widget-nav">{{_ 'menu'}} + <span class="icon-sm fa fa-chevron-circle-down toggle-menu-icon"></span> + </h3> + <ul class="nav-list"> + <hr style="margin-top: 0;"> + <li> + <a href="#" class="nav-list-item js-open-archive"> + <span class="icon-sm fa fa-archive icon-type"></span> + {{_ 'archived-items'}} + </a> + </li> + <li> + <a href="#" class="nav-list-item js-open-card-filter"> + <span class="icon-sm fa fa-filter icon-type"></span> + {{_ 'filter-cards'}} + </a> + </li> + {{#if currentUser.isBoardAdmin}} + <hr> + <li> + <a class="nav-list-item nav-list-sub-item board-settings-background js-change-background"> + <span class="board-settings-background-preview" style="background-color:{{board.background.color}}"></span> + {{_ 'change-background'}}… + </a> + </li> + {{#unless isSandstorm }} + <li> + <a class="nav-list-item nav-list-sub-item js-close-board" href="#">{{_ 'close-board'}}</a> + </li> + {{/unless}} + {{/if}} + {{! + XXX Language should be handled by sandstorm, but for now display a language selection link in the board menu. + This link is normally present in the header bar that is not displayed on sandstorm. + }} + {{#if isSandstorm}} + <hr> + <li> + <a class="nav-list-item nav-list-sub-item js-language">{{_ 'language'}}</a> + </li> + {{/if}} + </ul> + </div> +</template> + +<template name="membersWidget"> + <hr> + <div class="board-widget board-widget-members clearfix"> + <div class="board-widget-title"> + <h3>{{_ 'members'}}</h3> + </div> + <div class="board-widget-content"> + <div class="board-widget-members js-list-board-members clearfix js-list-draggable-board-members"> + {{# each board.members }} + {{> userAvatar userId=this.userId draggable=true size="small" showBadges=true}} + {{/ each }} + </div> + {{# unless isSandstrom }} + {{# if currentUser.isBoardAdmin }} + <a href="#" class="button-link js-open-manage-board-members"> + <span class="icon-sm fa fa-user"></span> {{_ 'add-members'}} + </a> + {{/ if }} + {{/ unless }} + </div> + </div> +</template> + +<template name="activityWidget"> + {{# if board.activities.count }} + <hr> + <div class="board-widget board-widget-activity bottom clearfix"> + <div class="board-widget-title"> + <h3>{{_ 'activity'}}</h3> + </div> + <div class="board-widget-content"> + <div class="activity-gradient-t"></div> + <div class="activity-gradient-b"></div> + <div class="board-actions-list fancy-scrollbar"> + {{ > activities }} + </div> + </div> + </div> + {{/if}} +</template> + +<template name="memberPopup"> + <div class="board-member-menu"> + <div class="mini-profile-info"> + {{> userAvatar user=user}} + <div class="info"> + <h3 class="bottom" style="margin-right: 40px;"> + <a class="js-profile" href="{{ pathFor route='Profile' username=user.username }}">{{ user.profile.name }}</a> + </h3> + <p class="quiet bottom">@{{ user.username }}</p> + </div> + </div> + {{# if currentUser.isBoardMember }} + <ul class="pop-over-list"> + {{# if currentUser.isBoardAdmin }} + <li> + <a class="js-change-role" href="#"> + {{_ 'change-permissions'}} <span class="quiet" style="font-weight: normal;">({{ memberType }})</span> + </a> + </li> + {{/ if }} + + <li> + {{# if currentUser.isBoardAdmin }} + <a class="js-remove-member">{{_ 'remove-from-board'}}</a> + {{ else }} + <a class="js-leave-member">{{_ 'leave-board'}}</a> + {{/ if }} + </li> + </ul> + {{/ if }} + </div> +</template> + +<template name="filterWidget"> + <ul class="pop-over-label-list checkable"> + {{#each board.labels}} + <li class="item matches-filter"> + <a class="name js-toggle-label-filter"> + <span class="card-label card-label-{{color}}"></span> + <span class="full-name"> + {{#if name}} + {{name}} + {{else}} + <span class="quiet">{{_ "label-default" color}}</span> + {{/if}} + </span> + {{#if Filter.labelIds.isSelected _id}} + <span class="icon-sm fa fa-check"></span> + {{/if}} + </a> + </li> + {{/each}} + </ul> + <hr> + <ul class="pop-over-member-list checkable"> + {{#each board.members}} + {{#with getUser userId}} + <li class="item js-member-item {{#if Filter.members.isSelected _id}}active{{/if}}"> + <a href="#" class="name js-toogle-member-filter"> + {{> userAvatar user=this size="small" }} + <span class="full-name"> + {{ profile.name }} + (<span class="username">{{ username }}</span>) + </span> + {{#if Filter.members.isSelected _id}} + <span class="icon-sm fa fa-check checked-icon"></span> + {{/if}} + </a> + </li> + {{/with}} + {{/each}} + </ul> + <hr> + <ul class="pop-over-list inset normal-weight"> + <li> + <a class="js-clear-all {{#unless Filter.isActive}}disabled{{/unless}}" style="padding-left: 40px;"> + {{_ 'filter-clear'}} + </a> + </li> + </ul> +</template> + +<template name="backgroundWidget"> + <div class="board-widgets-content-wrapper fancy-scrollbar"> + <div class="board-widgets-content"> + <div class="board-backgrounds-list clearfix"> + {{#each backgroundColors}} + <div class="board-background-select js-select-background"> + <span class="background-box " style="background-color: {{this}}; "></span> + </div> + {{/each}} + </div> + {{!-- + <h2 class="clear">Photos</h2> + <div class="board-backgrounds-list relative clearfix js-gold-photos-list disabled"> + <div class="board-background-select js-select-background"> + <span class="background-box " style="background-image: url("{{url}}");"> + <a class="background-option js-background-attribution" href={{href}} target="_blank" title={{title}}> + <img src="https://d78fikflryjgj.cloudfront.net/images/d906fe5c1274c56c5571d49705547587/cc.png" style="height: 14px; width: 14px; vertical-align: text-top;" title="http://creativecommons.org/licenses/by/2.0/deed.en"> + <span class="text" style="margin-left: 2px;">{{author}}</span> + </a> + </span> + </div> + </div> + --}} + </div> + </div> +</template> + +<template name="closeBoardPopup"> + <p>{{_ 'close-board-pop'}}</p> + <input type="submit" class="js-confirm negate full" value="{{_ 'close'}}"> +</template> + +<template name="removeMemberPopup"> + <p>{{_ 'remove-member-pop' + name=user.profile.name + username=user.username + boardTitle=board.title}}</p> + <input type="submit" class="js-confirm negate full" value="{{_ 'remove-member'}}"> +</template> + +<template name="addMemberPopup"> + <div class="search-with-spinner"> + {{> esInput index="users" }} + </div> + + <div class="manage-member-section hide js-search-results" style="display: block;"> + <ul class="pop-over-member-list options js-list"> + {{# esEach index="users"}} + <li class="item js-member-item {{# if isBoardMember }}disabled{{/if}}"> + <a href="#" class="name js-select-member {{# if isBoardMember }}multi-line{{/if}}" title="{{ profile.name }} ({{ username }})"> + {{> userAvatar user=this size="small" }} + <span class="full-name"> + {{ profile.name }} (<span class="username">{{ username }}</span>) + </span> + {{# if isBoardMember }} + <div class="extra-text quiet">({{_ 'joined'}})</div> + {{/if}} + <span class="icon-sm fa fa-chevron-right light option js-open-option"></span> + </a> + </li> + {{/esEach }} + </ul> + </div> + + {{# ifEsIsSearching index='users' }} + <div class="tac"> + <span class="tabbed-pane-main-col-loading-spinner spinner"></span> + </div> + {{ /ifEsIsSearching }} + + {{# ifEsHasNoResults index="users" }} + <div class="manage-member-section js-no-results"> + <p class="quiet center" style="padding: 16px 4px;">{{_ 'no-results'}}</p> + </div> + {{ /ifEsHasNoResults }} + + <div class="manage-member-section js-helper"> + <p class="bottom quiet" style="padding: 6px;">{{_ 'search-member-desc'}}</p> + </div> +</template> + +<template name="changePermissionsPopup"> + <ul class="pop-over-list"> + <li> + <a class="{{#if isLastAdmin}}disabled{{else}}js-set-admin{{/if}}"> + {{_ 'admin'}} + {{#if isAdmin}}<span class="icon-sm fa fa-check"></span>{{/if}} + <span class="sub-name">{{_ 'admin-desc'}}</span> + </a> + </li> + <li> + <a class="{{#if isLastAdmin}}disabled{{else}}js-set-normal{{/if}}"> + {{_ 'normal'}} + {{#unless isAdmin}}<span class="icon-sm fa fa-check"></span>{{/unless}} + <span class="sub-name">{{_ 'normal-desc'}}</span> + </a> + </li> + </ul> + {{#if isLastAdmin}} + <hr> + <p class="quiet bottom">{{_ 'last-admin-desc'}}</p> + {{/if}} +</template> diff --git a/client/components/sidebar/templates.jade b/client/components/sidebar/templates.jade new file mode 100644 index 00000000..23a1a87e --- /dev/null +++ b/client/components/sidebar/templates.jade @@ -0,0 +1,103 @@ +template(name="boardSidebar") + .board-sidebar.sidebar(class="{{#if isOpen}}is-open{{/if}}") + a.sidebar-tongue.js-toogle-sidebar( + class="{{#if isTongueHidden}}is-hidden{{/if}}") + i.fa.fa-chevron-left + .sidebar-content.js-board-sidebar-content + //- XXX https://github.com/peerlibrary/meteor-blaze-components/issues/30 + if Filter.isActive + +filterSidebar + else + +homeSidebar + +template(name='homeSidebar') + +membersWidget + hr.clear + +labelsWidget + hr.clear + h3 + i.fa.fa-comments-o + | {{_ 'activities'}} + +activities(mode="board") + +template(name="filterSidebar") + ul.pop-over-label-list.checkable + each currentBoard.labels + li.item.matches-filter + a.name.js-toggle-label-filter + span.card-label(class="card-label-{{color}}") + span.full-name + if name + = name + else + span.quiet {{_ "label-default" color}} + if Filter.labelIds.isSelected _id}} + span.icon-sm.fa.fa-check + hr + ul.pop-over-member-list.checkable + each currentBoard.members + if isActive + with getUser userId + li.item.js-member-item( + class="{{#if Filter.members.isSelected _id}}active{{/if}}") + a.name.js-toogle-member-filter + +userAvatar(user=this size="small") + span.full-name + = profile.name + | (<span class="username">{{ username }}</span>) + if Filter.members.isSelected _id + span.icon-sm.fa.fa-check + hr + a.js-clear-all(class="{{#unless Filter.isActive}}disabled{{/unless}}") + | {{_ 'filter-clear'}} + +template(name="membersWidget") + .board-widget.board-widget-members + h3 + i.fa.fa-user + | {{_ 'members'}} + .board-widget-content + each currentBoard.members + +userAvatar( + userId=this.userId + draggable=true + size="small" + showBadges=true) + unless isSandstorm + if currentUser.isBoardAdmin + a.js-open-manage-board-members + +template(name="labelsWidget") + .board-widget.board-widget-labels + h3 + i.fa.fa-tags + | {{_ 'labels'}} + .board-widget-content + each currentBoard.labels + a.card-label(class="card-label-{{color}}").js-label + span.card-label-name= name + a.card-label.js-add-label + i.fa.fa-plus + +template(name="memberPopup") + .board-member-menu: .mini-profile-info + +userAvatar(user=user) + .info + h3.bottom + a.js-profile(href="{{pathFor route='Profile' username=user.username}}") + = user.profile.name + p.quiet.bottom @#{user.username} + if currentUser.isBoardMember + ul.pop-over-list + li + a.js-filter-member Filter cards + if currentUser.isBoardAdmin + li + a.js-change-role + | {{_ 'change-permissions'}} + span.quiet (#{memberType}) + li + if currentUser.isBoardAdmin + a.js-remove-member {{_ 'remove-from-board'}} + else + a.js-leave-member {{_ 'leave-board'}} diff --git a/client/components/users/avatar.jade b/client/components/users/avatar.jade new file mode 100644 index 00000000..70ef69e0 --- /dev/null +++ b/client/components/users/avatar.jade @@ -0,0 +1,7 @@ +template(name="userAvatar") + .member(class="{{class}} {{# if draggable }}js-member{{else}}js-member-on-card-menu{{/if}}" + title="{{userData.profile.name}} ({{userData.username}})") + +avatar(user=userData size=size) + if showBadges + span.member-status(class="{{# if userData.profile.status}}active{{/if}}") + span.member-type(class=memberType) diff --git a/client/components/users/events.js b/client/components/users/events.js new file mode 100644 index 00000000..14df9717 --- /dev/null +++ b/client/components/users/events.js @@ -0,0 +1,59 @@ +// XXX This should be handled by default (and in a better way) by useraccounts. +// See https://github.com/meteor-useraccounts/core/issues/384 +Template.atForm.onRendered(function() { + this.find('input').focus(); +}); + +Template.memberMenuPopup.events({ + 'click .js-language': Popup.open('setLanguage'), + 'click .js-logout': function(evt) { + evt.preventDefault(); + + Meteor.logout(function() { + Router.go('Home'); + }); + } +}); + +Template.setLanguagePopup.events({ + 'click .js-set-language': function(evt) { + Users.update(Meteor.userId(), { + $set: { + 'profile.language': this.tag + } + }); + evt.preventDefault(); + } +}); + +Template.profileEditForm.events({ + 'click .js-edit-profile': function() { + Session.set('ProfileEditForm', true); + }, + 'click .js-cancel-edit-profile': function() { + Session.set('ProfileEditForm', false); + }, + 'submit #ProfileEditForm': function(evt, t) { + var name = t.find('#name').value; + var bio = t.find('#bio').value; + + // trim and update + if ($.trim(name)) { + Users.update(this.profile()._id, { + $set: { + 'profile.name': name, + 'profile.bio': bio + } + }, function() { + + // update complete close profileEditForm + Session.set('ProfileEditForm', false); + }); + } + evt.preventDefault(); + } +}); + +Template.memberName.events({ + 'click .js-show-mem-menu': Popup.open('user') +}); diff --git a/client/components/users/form.styl b/client/components/users/form.styl new file mode 100644 index 00000000..845c810d --- /dev/null +++ b/client/components/users/form.styl @@ -0,0 +1,50 @@ +.at-form-landing-logo + width: 275px + margin: auto + margin-top: 50px + margin-top: 17vh + + img + width: 275px + + +.at-form + margin: auto + width: 275px + padding: 25px + margin-top: 20px + padding-bottom: 10px + background: #fff + border-radius: 3px + border: 1px solid #dbdbdb + border-bottom-color: #c2c2c2 + box-shadow: 0 1px 6px rgba(0, 0, 0, .3) + + .at-link + color: darken(#27AE60, 40%) + + label + margin-bottom: 3px + + input + width: 100% + + .at-title + background: #F7F7F7 + margin: -25px + padding: 15px 25px 5px + margin-bottom: 20px + border-bottom: 1px solid #dcdcdc + color: darken(white, 70%) + font-weight: bold + + .at-signup-link, + .at-signin-link, + .at-forgotPwd + font-size: 0.9em + margin-top: 15px + color: darken(white, 70%) + + .at-signUp, + .at-signIn + font-weight: bold diff --git a/client/components/users/headerButtons.jade b/client/components/users/headerButtons.jade new file mode 100644 index 00000000..74c24ad5 --- /dev/null +++ b/client/components/users/headerButtons.jade @@ -0,0 +1,27 @@ +template(name="headerUserBar") + #header-user-bar + if currentUser + a.js-open-header-member-menu + if currentUser.profile.name + = currentUser.profile.name + else + = currentUser.username + i.fa.fa-chevron-down + else + a(href="{{pathFor route='signUp'}}") Sign in + span.separator - + a(href="{{pathFor route='signIn'}}") Log in + +template(name="memberHeader") + a.header-member.js-open-header-member-menu + span= currentUser.profile.name + +userAvatar(user=currentUser size="small") + +template(name="memberMenuPopup") + ul.pop-over-list + li: a(href="{{pathFor route='Profile' username=currentUser.username}}") {{_ 'profile'}} + li: a.js-language {{_ 'language'}} + li: a(href = "{{pathFor route='Settings'}}") {{_ 'settings'}} + hr + ul.pop-over-list + li: a.js-logout {{_ 'log-out'}} diff --git a/client/components/users/headerButtons.js b/client/components/users/headerButtons.js new file mode 100644 index 00000000..70594fb5 --- /dev/null +++ b/client/components/users/headerButtons.js @@ -0,0 +1,5 @@ +Template.headerUserBar.events({ + 'click .js-sign-in': Popup.open('signup'), + 'click .js-log-in': Popup.open('login'), + 'click .js-open-header-member-menu': Popup.open('memberMenu') +}); diff --git a/client/components/users/helpers.js b/client/components/users/helpers.js new file mode 100644 index 00000000..33867298 --- /dev/null +++ b/client/components/users/helpers.js @@ -0,0 +1,27 @@ +Template.userAvatar.helpers({ + userData: function() { + if (! this.user) { + this.user = Users.findOne(this.userId); + } + return this.user; + }, + memberType: function() { + var userId = this.userId || this.user._id; + var user = Users.findOne(userId); + return user && user.isBoardAdmin() ? 'admin' : 'normal'; + } +}); + +Template.setLanguagePopup.helpers({ + languages: function() { + return _.map(TAPi18n.getLanguages(), function(lang, tag) { + return { + tag: tag, + name: lang.name + }; + }); + }, + isCurrentLanguage: function() { + return this.tag === TAPi18n.getLanguage(); + } +}); diff --git a/client/components/users/member.styl b/client/components/users/member.styl new file mode 100644 index 00000000..3dfdaa37 --- /dev/null +++ b/client/components/users/member.styl @@ -0,0 +1,107 @@ +@import 'nib' + +avatar-radius = 50% + +.member + border-radius: 3px + display: block + float: left + height: 30px + width: @height + margin: 0 4px 4px 0 + position: relative + cursor: pointer + user-select: none + z-index: 1 + text-decoration: none + border-radius: avatar-radius + + .avatar + height: 100% + width: @height + display: flex + align-items: center + justify-content: center + overflow: hidden + border-radius: avatar-radius + + .avatar-initials + font-weight: bold + max-width: 100% + max-height: 100% + font-size: 14px + line-height: 200% + background-color: #dbdbdb + color: #444444 + + .avatar-image + max-width: 100% + max-height: 100% + + .member-status + background-color: #b3b3b3 + border: 1px solid #fff + border-radius: 50% + height: 8px + width: @height + position: absolute + right: 0px + bottom: 0px + border: 1px solid white + + &.active + background: #64c464 + border-color: #daf1da + + &.idle + background: #e4e467 + border-color: #f7f7d4 + + &.disconnected + background: #bdbdbd + border-color: #ededed + + &.extra-small + .avatar-initials + font-size: 9px + width: 18px + height: 18px + line-height: 18px + + .avatar-image + width: 18px + height: 18px + + &.small + width: 30px + height: 30px + + .avatar-initials + font-size: 12px + line-height: 30px + + &.large + height: 85px + line-height: 85px + width: 85px + + .avatar + width: 85px + height: 85px + + .avatar-initials + font-size: 16px + font-weight: 700 + line-height: 85px + width: 85px + +.atMention + background: #dbdbdb + border-radius: 3px + padding: 1px 4px + margin: -1px 0 + display: inline-block + + &.me + background: #cfdfe8 + diff --git a/client/components/users/router.js b/client/components/users/router.js new file mode 100644 index 00000000..d59e174d --- /dev/null +++ b/client/components/users/router.js @@ -0,0 +1,29 @@ + +_.each(['signIn', 'signUp', 'resetPwd', + 'forgotPwd', 'enrollAccount', 'changePwd'], function(routeName) { + AccountsTemplates.configureRoute(routeName, { + layoutTemplate: 'userFormsLayout' + }); +}); + +Router.route('/profile/:username', { + name: 'Profile', + template: 'profile', + waitOn: function() { + return Meteor.subscribe('profile', this.params.username); + }, + data: function() { + var params = this.params; + return { + profile: function() { + return Users.findOne({ username: params.username }); + } + }; + } +}); + +Router.route('/settings', { + name: 'Settings', + template: 'settings', + layoutTemplate: 'AuthLayout' +}); diff --git a/client/components/users/templates.html b/client/components/users/templates.html new file mode 100644 index 00000000..5783eebf --- /dev/null +++ b/client/components/users/templates.html @@ -0,0 +1,118 @@ +<template name="setLanguagePopup"> +<ul class="pop-over-list"> + {{#each languages}} + <li class="{{# if isCurrentLanguage}}active{{/if}}"> + <a class="js-set-language"> + {{name}} + {{# if isCurrentLanguage}} + <span class="icon-sm fa fa-check"></span> + {{/if}} + </a> + </li> + {{/each}} +</ul> +</template> + +<template name='profile'> + {{ # if profile }} + <div class="tabbed-pane-header"> + <div class="tabbed-pane-header-wrapper clearfix"> + <a class="tabbed-pane-header-image profile-image ed js-change-avatar-profile" href="#"> + {{> userAvatar user=profile size="large"}} + </a> + <div class="tabbed-pane-header-details"> + <div class="js-current-details"> + <div class="tabbed-pane-header-details-name"> + <h1 class="inline"> {{ profile.profile.name }} </h1> + <p class="window-title-extra quiet"> @{{ profile.username }} </p> + </div> + <div class="tabbed-pane-header-details-content"> + <p>{{ profile.profile.bio }}</p> + </div> + <div class="tabbed-pane-header-details-content"></div> + </div> + {{ > profileEditForm }} + </div> + </div> + </div> + {{ else }} + {{ > message label='user-profile-not-found' }} + {{ /if }} +</template> + +<template name="settings"> + {{ > profile profile=currentUser }} + <div class="tabbed-pane-main-col clearfix"> + <div class="tabbed-pane-main-col-loading hide js-loading-page"> + <span class="tabbed-pane-main-col-loading-spinner spinner"></span> + </div> + <div class="tabbed-pane-main-col-wrapper js-content"> + <div class="window-module clearfix"> + <div class="window-module-title"> + <h3>{{_ "account-details"}}</h3> + </div> + <a class="big-link js-change-name-and-bio" href="#"> + <span class="text">{{_ 'change-name-initials-bio'}}</span> + </a> + <a class="big-link js-change-avatar" href="#"> + <span class="text">{{_ 'change-avatar'}}</span> + </a> + <a class="big-link js-change-password" href="#"> + <span class="text">{{_ 'change-password'}}</span> + </a> + <a class="big-link js-change-email" href="#"> + <span class="text">{{_ 'change-email'}}</span> + </a> + </div> + </div> + </div> +</template> + +<template name="profileEditForm"> + {{#if $eq currentUser.username profile.username }} + {{# if session 'ProfileEditForm' }} + <form id="ProfileEditForm" class="js-profile-form"> + <p class="error js-profile-form-error hide"></p> + <label>{{_ "username"}}</label> + <input type="text" id="username" value="{{ profile.username }}" disabled> + <label>{{_ "fullname"}}</label> + <input type="text" id="name" value="{{ profile.profile.name }}"> + <label> + {{_ "bio"}} <span class="quiet">({{_ 'optional'}})</span> + </label> + <textarea id="bio">{{ profile.profile.bio }}</textarea> + <input type="submit" class="primary wide js-submit-profile" value="{{_ 'save'}}"> + <input type="button" class="js-cancel-edit-profile" value="{{_ 'cancel'}}"> + </form> + {{ else }} + <a class="button-link tabbed-pane-header-details-edit js-edit-profile" href="#"> + <span class="icon-sm fa fa-pencil"></span> + {{_ "edit-profile"}} + </a> + {{ /if }} + {{ /if }} +</template> + +<template name="userPopup"> + <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> + </div> +</template> + + +<template name="memberName"> + <a class="inline-object js-show-mem-menu" href="{{ pathFor route='Profile' username=user.username }}"> + {{ user.profile.name }} + {{# if username }} + ({{ user.username }}) + {{ /if }} + </a> +</template> diff --git a/client/config/accounts.js b/client/config/accounts.js new file mode 100644 index 00000000..9e0d17d3 --- /dev/null +++ b/client/config/accounts.js @@ -0,0 +1,35 @@ +AccountsTemplates.configure({ + confirmPassword: false, + enablePasswordChange: true, + sendVerificationEmail: true, + showForgotPasswordLink: true +}); + +AccountsTemplates.removeField('password'); +AccountsTemplates.removeField('email'); +AccountsTemplates.addFields([ + { + _id: 'username', + type: 'text', + displayName: 'username', + required: true, + minLength: 5 + }, + { + _id: 'email', + type: 'email', + required: true, + displayName: 'email', + re: /.+@(.+){2,}\.(.+){2,}/, + errStr: 'Invalid email' + }, + { + _id: 'password', + type: 'password', + placeholder: { + signUp: 'At least six characters' + }, + required: true, + minLength: 6 + } +]); diff --git a/client/config/avatar.js b/client/config/avatar.js new file mode 100644 index 00000000..fc4ba58b --- /dev/null +++ b/client/config/avatar.js @@ -0,0 +1,3 @@ +Avatar.options = { + fallbackType: 'initials' +}; diff --git a/client/config/router.js b/client/config/router.js new file mode 100644 index 00000000..c859013f --- /dev/null +++ b/client/config/router.js @@ -0,0 +1,28 @@ +Router.configure({ + loadingTemplate: 'spinner', + notFoundTemplate: 'notfound', + layoutTemplate: 'defaultLayout', + + onBeforeAction: function() { + var options = this.route.options; + + // Redirect logged in users to Boards view when they try to open Login or + // signup views. + if (Meteor.userId() && options.redirectLoggedInUsers) { + return this.redirect('Boards'); + } + + // Authenticated + if (! Meteor.userId() && options.authenticated) { + return this.redirect('atSignIn'); + } + + // Reset default sessions + Session.set('error', false); + Session.set('warning', false); + + Popup.close(); + + this.next(); + } +}); diff --git a/client/lib/emoji-values.js b/client/lib/emoji-values.js new file mode 100644 index 00000000..1f07ac62 --- /dev/null +++ b/client/lib/emoji-values.js @@ -0,0 +1,152 @@ +Emoji.values = ['+1', '-1', '100', '1234', '8ball', 'a', 'ab', 'abc', 'abcd', +'accept', 'aerial_tramway', 'airplane', 'alarm_clock', 'alien', 'ambulance', +'anchor', 'angel', 'anger', 'angry', 'anguished', 'ant', 'apple', 'aquarius', +'aries', 'arrow_backward', 'arrow_double_down', 'arrow_double_up', 'arrow_down', +'arrow_down_small', 'arrow_forward', 'arrow_heading_down', 'arrow_heading_up', +'arrow_left', 'arrow_lower_left', 'arrow_lower_right', 'arrow_right', +'arrow_right_hook', 'arrow_up', 'arrow_up_down', 'arrow_up_small', +'arrow_upper_left', 'arrow_upper_right', 'arrows_clockwise', +'arrows_counterclockwise', 'art', 'articulated_lorry', 'astonished', 'atm', 'b', +'baby', 'baby_bottle', 'baby_chick', 'baby_symbol', 'baggage_claim', 'balloon', +'ballot_box_with_check', 'bamboo', 'banana', 'bangbang', 'bank', 'bar_chart', +'barber', 'baseball', 'basketball', 'bath', 'bathtub', 'battery', 'bear', 'bee', +'beer', 'beers', 'beetle', 'beginner', 'bell', 'bento', 'bicyclist', 'bike', +'bikini', 'bird', 'birthday', 'black_circle', 'black_joker', 'black_nib', +'black_square', 'black_square_button', 'blossom', 'blowfish', 'blue_book', +'blue_car', 'blue_heart', 'blush', 'boar', 'boat', 'bomb', 'book', 'bookmark', +'bookmark_tabs', 'books', 'boom', 'boot', 'bouquet', 'bow', 'bowling', 'bowtie', +'boy', 'bread', 'bride_with_veil', 'bridge_at_night', 'briefcase', +'broken_heart', 'bug', 'bulb', 'bullettrain_front', 'bullettrain_side', 'bus', +'busstop', 'bust_in_silhouette', 'busts_in_silhouette', 'cactus', 'cake', +'calendar', 'calling', 'camel', 'camera', 'cancer', 'candy', 'capital_abcd', +'capricorn', 'car', 'card_index', 'carousel_horse', 'cat', 'cat2', 'cd', +'chart', 'chart_with_downwards_trend', 'chart_with_upwards_trend', +'checkered_flag', 'cherries', 'cherry_blossom', 'chestnut', 'chicken', +'children_crossing', 'chocolate_bar', 'christmas_tree', 'church', 'cinema', +'circus_tent', 'city_sunrise', 'city_sunset', 'cl', 'clap', 'clapper', +'clipboard', 'clock1', 'clock10', 'clock1030', 'clock11', 'clock1130', +'clock12', 'clock1230', 'clock130', 'clock2', 'clock230', 'clock3', 'clock330', +'clock4', 'clock430', 'clock5', 'clock530', 'clock6', 'clock630', 'clock7', +'clock730', 'clock8', 'clock830', 'clock9', 'clock930', 'closed_book', +'closed_lock_with_key', 'closed_umbrella', 'cloud', 'clubs', 'cn', 'cocktail', +'coffee', 'cold_sweat', 'collision', 'computer', 'confetti_ball', 'confounded', +'confused', 'congratulations', 'construction', 'construction_worker', +'convenience_store', 'cookie', 'cool', 'cop', 'copyright', 'corn', 'couple', +'couple_with_heart', 'couplekiss', 'cow', 'cow2', 'credit_card', 'crocodile', +'crossed_flags', 'crown', 'cry', 'crying_cat_face', 'crystal_ball', 'cupid', +'curly_loop', 'currency_exchange', 'curry', 'custard', 'customs', 'cyclone', +'dancer', 'dancers', 'dango', 'dart', 'dash', 'date', 'de', 'deciduous_tree', +'department_store', 'diamond_shape_with_a_dot_inside', 'diamonds', +'disappointed', 'disappointed_relieved', 'dizzy', 'dizzy_face', 'do_not_litter', +'dog', 'dog2', 'dollar', 'dolls', 'dolphin', 'donut', 'door', 'doughnut', +'dragon', 'dragon_face', 'dress', 'dromedary_camel', 'droplet', 'dvd', 'e-mail', +'ear', 'ear_of_rice', 'earth_africa', 'earth_americas', 'earth_asia', 'egg', +'eggplant', 'eight', 'eight_pointed_black_star', 'eight_spoked_asterisk', +'electric_plug', 'elephant', 'email', 'end', 'envelope', 'es', 'euro', +'european_castle', 'european_post_office', 'evergreen_tree', 'exclamation', +'expressionless', 'eyeglasses', 'eyes', 'facepunch', 'factory', 'fallen_leaf', +'family', 'fast_forward', 'fax', 'fearful', 'feelsgood', 'feet', 'ferris_wheel', +'file_folder', 'finnadie', 'fire', 'fire_engine', 'fireworks', +'first_quarter_moon', 'first_quarter_moon_with_face', 'fish', 'fish_cake', +'fishing_pole_and_fish', 'fist', 'five', 'flags', 'flashlight', 'floppy_disk', +'flower_playing_cards', 'flushed', 'foggy', 'football', 'fork_and_knife', +'fountain', 'four', 'four_leaf_clover', 'fr', 'free', 'fried_shrimp', 'fries', +'frog', 'frowning', 'fu', 'fuelpump', 'full_moon', 'full_moon_with_face', +'game_die', 'gb', 'gem', 'gemini', 'ghost', 'gift', 'gift_heart', 'girl', +'globe_with_meridians', 'goat', 'goberserk', 'godmode', 'golf', 'grapes', +'green_apple', 'green_book', 'green_heart', 'grey_exclamation', 'grey_question', +'grimacing', 'grin', 'grinning', 'guardsman', 'guitar', 'gun', 'haircut', +'hamburger', 'hammer', 'hamster', 'hand', 'handbag', 'hankey', 'hash', +'hatched_chick', 'hatching_chick', 'headphones', 'hear_no_evil', 'heart', +'heart_decoration', 'heart_eyes', 'heart_eyes_cat', 'heartbeat', 'heartpulse', +'hearts', 'heavy_check_mark', 'heavy_division_sign', 'heavy_dollar_sign', +'heavy_exclamation_mark', 'heavy_minus_sign', 'heavy_multiplication_x', +'heavy_plus_sign', 'helicopter', 'herb', 'hibiscus', 'high_brightness', +'high_heel', 'hocho', 'honey_pot', 'honeybee', 'horse', 'horse_racing', +'hospital', 'hotel', 'hotsprings', 'hourglass', 'hourglass_flowing_sand', +'house', 'house_with_garden', 'hurtrealbad', 'hushed', 'ice_cream', 'icecream', +'id', 'ideograph_advantage', 'imp', 'inbox_tray', 'incoming_envelope', +'information_desk_person', 'information_source', 'innocent', 'interrobang', +'iphone', 'it', 'izakaya_lantern', 'jack_o_lantern', 'japan', 'japanese_castle', +'japanese_goblin', 'japanese_ogre', 'jeans', 'joy', 'joy_cat', 'jp', 'key', +'keycap_ten', 'kimono', 'kiss', 'kissing', 'kissing_cat', 'kissing_closed_eyes', +'kissing_face', 'kissing_heart', 'kissing_smiling_eyes', 'koala', 'koko', 'kr', +'large_blue_circle', 'large_blue_diamond', 'large_orange_diamond', +'last_quarter_moon', 'last_quarter_moon_with_face', 'laughing', 'leaves', +'ledger', 'left_luggage', 'left_right_arrow', 'leftwards_arrow_with_hook', +'lemon', 'leo', 'leopard', 'libra', 'light_rail', 'link', 'lips', 'lipstick', +'lock', 'lock_with_ink_pen', 'lollipop', 'loop', 'loudspeaker', 'love_hotel', +'love_letter', 'low_brightness', 'm', 'mag', 'mag_right', 'mahjong', 'mailbox', +'mailbox_closed', 'mailbox_with_mail', 'mailbox_with_no_mail', 'man', +'man_with_gua_pi_mao', 'man_with_turban', 'mans_shoe', 'maple_leaf', 'mask', +'massage', 'meat_on_bone', 'mega', 'melon', 'memo', 'mens', 'metal', 'metro', +'microphone', 'microscope', 'milky_way', 'minibus', 'minidisc', +'mobile_phone_off', 'money_with_wings', 'moneybag', 'monkey', 'monkey_face', +'monorail', 'moon', 'mortar_board', 'mount_fuji', 'mountain_bicyclist', +'mountain_cableway', 'mountain_railway', 'mouse', 'mouse2', 'movie_camera', +'moyai', 'muscle', 'mushroom', 'musical_keyboard', 'musical_note', +'musical_score', 'mute', 'nail_care', 'name_badge', 'neckbeard', 'necktie', +'negative_squared_cross_mark', 'neutral_face', 'new', 'new_moon', +'new_moon_with_face', 'newspaper', 'ng', 'nine', 'no_bell', 'no_bicycles', +'no_entry', 'no_entry_sign', 'no_good', 'no_mobile_phones', 'no_mouth', +'no_pedestrians', 'no_smoking', 'non-potable_water', 'nose', 'notebook', +'notebook_with_decorative_cover', 'notes', 'nut_and_bolt', 'o', 'o2', 'ocean', +'octocat', 'octopus', 'oden', 'office', 'ok', 'ok_hand', 'ok_woman', +'older_man', 'older_woman', 'on', 'oncoming_automobile', 'oncoming_bus', +'oncoming_police_car', 'oncoming_taxi', 'one', 'open_file_folder', 'open_hands', +'open_mouth', 'ophiuchus', 'orange_book', 'outbox_tray', 'ox', 'page_facing_up', +'page_with_curl', 'pager', 'palm_tree', 'panda_face', 'paperclip', 'parking', +'part_alternation_mark', 'partly_sunny', 'passport_control', 'paw_prints', +'peach', 'pear', 'pencil', 'pencil2', 'penguin', 'pensive', 'performing_arts', +'persevere', 'person_frowning', 'person_with_blond_hair', +'person_with_pouting_face', 'phone', 'pig', 'pig2', 'pig_nose', 'pill', +'pineapple', 'pisces', 'pizza', 'plus1', 'point_down', 'point_left', +'point_right', 'point_up', 'point_up_2', 'police_car', 'poodle', 'poop', +'post_office', 'postal_horn', 'postbox', 'potable_water', 'pouch', +'poultry_leg', 'pound', 'pouting_cat', 'pray', 'princess', 'punch', +'purple_heart', 'purse', 'pushpin', 'put_litter_in_its_place', 'question', +'rabbit', 'rabbit2', 'racehorse', 'radio', 'radio_button', 'rage', 'rage1', +'rage2', 'rage3', 'rage4', 'railway_car', 'rainbow', 'raised_hand', +'raised_hands', 'raising_hand', 'ram', 'ramen', 'rat', 'recycle', 'red_car', +'red_circle', 'registered', 'relaxed', 'relieved', 'repeat', 'repeat_one', +'restroom', 'revolving_hearts', 'rewind', 'ribbon', 'rice', 'rice_ball', +'rice_cracker', 'rice_scene', 'ring', 'rocket', 'roller_coaster', 'rooster', +'rose', 'rotating_light', 'round_pushpin', 'rowboat', 'ru', 'rugby_football', +'runner', 'running', 'running_shirt_with_sash', 'sa', 'sagittarius', 'sailboat', +'sake', 'sandal', 'santa', 'satellite', 'satisfied', 'saxophone', 'school', +'school_satchel', 'scissors', 'scorpius', 'scream', 'scream_cat', 'scroll', +'seat', 'secret', 'see_no_evil', 'seedling', 'seven', 'shaved_ice', 'sheep', +'shell', 'ship', 'shipit', 'shirt', 'shit', 'shoe', 'shower', 'signal_strength', +'six', 'six_pointed_star', 'ski', 'skull', 'sleeping', 'sleepy', 'slot_machine', +'small_blue_diamond', 'small_orange_diamond', 'small_red_triangle', +'small_red_triangle_down', 'smile', 'smile_cat', 'smiley', 'smiley_cat', +'smiling_imp', 'smirk', 'smirk_cat', 'smoking', 'snail', 'snake', 'snowboarder', +'snowflake', 'snowman', 'sob', 'soccer', 'soon', 'sos', 'sound', +'space_invader', 'spades', 'spaghetti', 'sparkler', 'sparkles', +'sparkling_heart', 'speak_no_evil', 'speaker', 'speech_balloon', 'speedboat', +'squirrel', 'star', 'star2', 'stars', 'station', 'statue_of_liberty', +'steam_locomotive', 'stew', 'straight_ruler', 'strawberry', 'stuck_out_tongue', +'stuck_out_tongue_closed_eyes', 'stuck_out_tongue_winking_eye', 'sun_with_face', +'sunflower', 'sunglasses', 'sunny', 'sunrise', 'sunrise_over_mountains', +'surfer', 'sushi', 'suspect', 'suspension_railway', 'sweat', 'sweat_drops', +'sweat_smile', 'sweet_potato', 'swimmer', 'symbols', 'syringe', 'tada', +'tanabata_tree', 'tangerine', 'taurus', 'taxi', 'tea', 'telephone', +'telephone_receiver', 'telescope', 'tennis', 'tent', 'thought_balloon', 'three', +'thumbsdown', 'thumbsup', 'ticket', 'tiger', 'tiger2', 'tired_face', 'tm', +'toilet', 'tokyo_tower', 'tomato', 'tongue', 'top', 'tophat', 'tractor', +'traffic_light', 'train', 'train2', 'tram', 'triangular_flag_on_post', +'triangular_ruler', 'trident', 'triumph', 'trolleybus', 'trollface', 'trophy', +'tropical_drink', 'tropical_fish', 'truck', 'trumpet', 'tshirt', 'tulip', +'turtle', 'tv', 'twisted_rightwards_arrows', 'two', 'two_hearts', +'two_men_holding_hands', 'two_women_holding_hands', 'u5272', 'u5408', 'u55b6', +'u6307', 'u6708', 'u6709', 'u6e80', 'u7121', 'u7533', 'u7981', 'u7a7a', 'uk', +'umbrella', 'unamused', 'underage', 'unlock', 'up', 'us', 'v', +'vertical_traffic_light', 'vhs', 'vibration_mode', 'video_camera', 'video_game', +'violin', 'virgo', 'volcano', 'vs', 'walking', 'waning_crescent_moon', +'waning_gibbous_moon', 'warning', 'watch', 'water_buffalo', 'watermelon', +'wave', 'wavy_dash', 'waxing_crescent_moon', 'waxing_gibbous_moon', 'wc', +'weary', 'wedding', 'whale', 'whale2', 'wheelchair', 'white_check_mark', +'white_circle', 'white_flower', 'white_square', 'white_square_button', +'wind_chime', 'wine_glass', 'wink', 'wolf', 'woman', 'womans_clothes', +'womans_hat', 'womens', 'worried', 'wrench', 'x', 'yellow_heart', 'yen', 'yum', +'zap', 'zero', 'zzz']; diff --git a/client/lib/filter.js b/client/lib/filter.js new file mode 100644 index 00000000..507a2bb7 --- /dev/null +++ b/client/lib/filter.js @@ -0,0 +1,133 @@ +// Filtered view manager +// We define local filter objects for each different type of field (SetFilter, +// RangeFilter, dateFilter, etc.). We then define a global `Filter` object whose +// goal is to filter complete documents by using the local filters for each +// fields. + +// Use a "set" filter for a field that is a set of documents uniquely +// identified. For instance `{ labels: ['labelA', 'labelC', 'labelD'] }`. +var SetFilter = function() { + this._dep = new Tracker.Dependency(); + this._selectedElements = []; +}; + +_.extend(SetFilter.prototype, { + isSelected: function(val) { + this._dep.depend(); + return this._selectedElements.indexOf(val) > -1; + }, + + add: function(val) { + if (this.indexOfVal(val) === -1) { + this._selectedElements.push(val); + this._dep.changed(); + } + }, + + remove: function(val) { + var indexOfVal = this._indexOfVal(val); + if (this.indexOfVal(val) !== -1) { + this._selectedElements.splice(indexOfVal, 1); + this._dep.changed(); + } + }, + + toogle: function(val) { + var indexOfVal = this._indexOfVal(val); + if (indexOfVal === -1) { + this._selectedElements.push(val); + } else { + this._selectedElements.splice(indexOfVal, 1); + } + + this._dep.changed(); + }, + + reset: function() { + this._selectedElements = []; + this._dep.changed(); + }, + + _indexOfVal: function(val) { + return this._selectedElements.indexOf(val); + }, + + _isActive: function() { + this._dep.depend(); + return this._selectedElements.length !== 0; + }, + + _getMongoSelector: function() { + this._dep.depend(); + return { $in: this._selectedElements }; + } +}); + +// The global Filter object. +// XXX It would be possible to re-write this object more elegantly, and removing +// the need to provide a list of `_fields`. We also should move methods into the +// object prototype. +Filter = { + // XXX I would like to rename this field into `labels` to be consistent with + // the rest of the schema, but we need to set some migrations architecture + // before changing the schema. + labelIds: new SetFilter(), + members: new SetFilter(), + + _fields: ['labelIds', 'members'], + + // We don't filter cards that have been added after the last filter change. To + // implement this we keep the id of these cards in this `_exceptions` fields + // and use a `$or` condition in the mongo selector we return. + _exceptions: [], + _exceptionsDep: new Tracker.Dependency(), + + isActive: function() { + var self = this; + return _.any(self._fields, function(fieldName) { + return self[fieldName]._isActive(); + }); + }, + + getMongoSelector: function() { + var self = this; + + if (! self.isActive()) + return {}; + + var filterSelector = {}; + _.forEach(self._fields, function(fieldName) { + var filter = self[fieldName]; + if (filter._isActive()) + filterSelector[fieldName] = filter._getMongoSelector(); + }); + + var exceptionsSelector = {_id: {$in: this._exceptions}}; + this._exceptionsDep.depend(); + + return {$or: [filterSelector, exceptionsSelector]}; + }, + + reset: function() { + var self = this; + _.forEach(self._fields, function(fieldName) { + var filter = self[fieldName]; + filter.reset(); + }); + self.resetExceptions(); + }, + + addException: function(_id) { + if (this.isActive()) { + this._exceptions.push(_id); + this._exceptionsDep.changed(); + } + }, + + resetExceptions: function() { + this._exceptions = []; + this._exceptionsDep.changed(); + } +}; + +Blaze.registerHelper('Filter', Filter); diff --git a/client/lib/i18n.js b/client/lib/i18n.js new file mode 100644 index 00000000..7d7e3ebb --- /dev/null +++ b/client/lib/i18n.js @@ -0,0 +1,22 @@ +// We save the user language preference in the user profile, and use that to set +// the language reactively. If the user is not connected we use the language +// information provided by the browser, and default to english. + +Tracker.autorun(function() { + var language; + var currentUser = Meteor.user(); + if (currentUser) { + language = currentUser.profile && currentUser.profile.language; + } else { + language = navigator.language || navigator.userLanguage; + } + + if (language) { + + TAPi18n.setLanguage(language); + + // XXX + var shortLanguage = language.split('-')[0]; + T9n.setLanguage(shortLanguage); + } +}); diff --git a/client/lib/keyboard.js b/client/lib/keyboard.js new file mode 100644 index 00000000..c1267938 --- /dev/null +++ b/client/lib/keyboard.js @@ -0,0 +1,55 @@ +// XXX Pressing `?` should display a list of all shortcuts available. +// +// XXX There is no reason to define these shortcuts globally, they should be +// attached to a template (most of them will go in the `board` template). + +// Pressing `Escape` should close the last opened “element” and only the last +// one -- curently we handle popups and the card detailed view of the sidebar. +Mousetrap.bind('esc', function() { + if (currentlyOpenedForm.get() !== null) { + currentlyOpenedForm.get().close(); + + } else if (Popup.isOpen()) { + Popup.back(); + + // XXX We should have a higher level API + } else if (Session.get('currentCard')) { + Utils.goBoardId(Session.get('currentBoard')); + } +}); + +Mousetrap.bind('w', function() { + if (! Session.get('currentCard')) { + Sidebar.toogle(); + } else { + Utils.goBoardId(Session.get('currentBoard')); + Sidebar.hide(); + } +}); + +Mousetrap.bind('q', function() { + var currentBoardId = Session.get('currentBoard'); + var currentUserId = Meteor.userId(); + if (currentBoardId && currentUserId) { + Filter.members.toogle(currentUserId); + } +}); + +Mousetrap.bind('x', function() { + if (Filter.isActive()) { + Filter.reset(); + } +}); + +Mousetrap.bind(['down', 'up'], function(evt, key) { + if (! Session.get('currentCard')) { + return; + } + + var nextFunc = (key === 'down' ? 'next' : 'prev'); + var nextCard = $('.js-minicard.is-selected')[nextFunc]('.js-minicard').get(0); + if (nextCard) { + var nextCardId = Blaze.getData(nextCard)._id; + Utils.goCardId(nextCardId); + } +}); diff --git a/client/lib/mixins.js b/client/lib/mixins.js new file mode 100644 index 00000000..8d16be53 --- /dev/null +++ b/client/lib/mixins.js @@ -0,0 +1 @@ +Mixins = {}; diff --git a/client/lib/popup.js b/client/lib/popup.js new file mode 100644 index 00000000..dd2a43b0 --- /dev/null +++ b/client/lib/popup.js @@ -0,0 +1,200 @@ +// A simple tracker dependency that we invalidate every time the window is +// resized. This is used to reactively re-calculate the popup position in case +// of a window resize. +var windowResizeDep = new Tracker.Dependency(); +$(window).on('resize', function() { windowResizeDep.changed(); }); + +Popup = { + /// This function returns a callback that can be used in an event map: + /// + /// Template.tplName.events({ + /// 'click .elementClass': Popup.open("popupName") + /// }); + /// + /// The popup inherit the data context of its parent. + open: function(name) { + var self = this; + var popupName = name + 'Popup'; + + return function(evt) { + // If a popup is already openened, clicking again on the opener element + // should close it -- and interupt the current `open` function. + if (self.isOpen() && + self._getTopStack().openerElement === evt.currentTarget) { + return self.close(); + } + + // We determine the `openerElement` (the DOM element that is being clicked + // and the one we take in reference to position the popup) from the event + // if the popup has no parent, or from the parent `openerElement` if it + // has one. This allows us to position a sub-popup exactly at the same + // position than its parent. + var openerElement; + if (self._hasPopupParent()) { + openerElement = self._getTopStack().openerElement; + } else { + self._stack = []; + openerElement = evt.currentTarget; + } + + // We modify the event to prevent the popup being closed when the event + // bubble up to the document element. + evt.originalEvent.clickInPopup = true; + evt.preventDefault(); + + // We push our popup data to the stack. The top of the stack is always + // used as the data source for our current popup. + self._stack.push({ + __isPopup: true, + popupName: popupName, + hasPopupParent: self._hasPopupParent(), + title: self._getTitle(popupName), + openerElement: openerElement, + offset: self._getOffset(openerElement), + dataContext: this.currentData && this.currentData() || this + }); + + // If there are no popup currently opened we use the Blaze API to render + // one into the DOM. We use a reactive function as the data parameter that + // just return the top element on the stack and depends on our internal + // dependency that is being invalidated every time the top element of the + // stack has changed and we want to update the popup. + // + // Otherwise if there is already a popup open we just need to invalidate + // our internal dependency, and since we just changed the top element of + // our internal stack, the popup will be updated with the new data. + if (! self.isOpen()) { + self.current = Blaze.renderWithData(self.template, function() { + self._dep.depend(); + return self._stack[self._stack.length - 1]; + }, document.body); + + } else { + self._dep.changed(); + } + }; + }, + + /// This function returns a callback that can be used in an event map: + /// + /// Template.tplName.events({ + /// 'click .elementClass': Popup.afterConfirm("popupName", function() { + /// // What to do after the user has confirmed the action + /// }) + /// }); + afterConfirm: function(name, action) { + var self = this; + + return function(evt, tpl) { + var context = this; + context.__afterConfirmAction = action; + self.open(name).call(context, evt, tpl); + }; + }, + + /// The public reactive state of the popup. + isOpen: function() { + this._dep.changed(); + return !! this.current; + }, + + /// In case the popup was opened from a parent popup we can get back to it + /// with this `Popup.back()` function. You can go back several steps at once + /// by providing a number to this function, e.g. `Popup.back(2)`. In this case + /// intermediate popup won't even be rendered on the DOM. If the number of + /// steps back is greater than the popup stack size, the popup will be closed. + back: function(n) { + n = n || 1; + var self = this; + if (self._stack.length > n) { + _.times(n, function() { self._stack.pop(); }); + self._dep.changed(); + } else { + self.close(); + } + }, + + /// Close the current opened popup. + close: function() { + if (this.isOpen()) { + Blaze.remove(this.current); + this.current = null; + this._stack = []; + } + }, + + // The template we use for every popup + template: Template.popup, + + // We only want to display one popup at a time and we keep the view object in + // this `Popup._current` variable. If there is no popup currently opened the + // value is `null`. + _current: null, + + // It's possible to open a sub-popup B from a popup A. In that case we keep + // the data of popup A so we can return back to it. Every time we open a new + // popup the stack grows, every time we go back the stack decrease, and if we + // close the popup the stack is reseted to the empty stack []. + _stack: [], + + // We invalidate this internal dependency every time the top of the stack has + // changed and we want to render a popup with the new top-stack data. + _dep: new Tracker.Dependency(), + + // An utility fonction that returns the top element of the internal stack + _getTopStack: function() { + return this._stack[this._stack.length - 1]; + }, + + // We use the blaze API to determine if the current popup has been opened from + // a parent popup. The number we give to the `Template.parentData` has been + // determined experimentally and is susceptible to change if you modify the + // `Popup.template` + _hasPopupParent: function() { + var tryParentData = Template.parentData(3); + return !! (tryParentData && tryParentData.__isPopup); + }, + + // We automatically calculate the popup offset from the reference element + // position and dimensions. We also reactively use the window dimensions to + // ensure that the popup is always visible on the screen. + _getOffset: function(element) { + var $element = $(element); + return function() { + windowResizeDep.depend(); + var offset = $element.offset(); + var popupWidth = 300 + 15; + return { + left: Math.min(offset.left, $(window).width() - popupWidth), + top: offset.top + $element.outerHeight() + }; + }; + }, + + // We get the title from the translation files. Instead of returning the + // result, we return a function that compute the result and since `TAPi18n.__` + // is a reactive data source, the title will be changed reactively. + _getTitle: function(popupName) { + return function() { + var translationKey = popupName + '-title'; + + // XXX There is no public API to check if there is an available + // translation for a given key. So we try to translate the key and if the + // translation output equals the key input we deduce that no translation + // was available and returns `false`. There is a (small) risk a false + // positives. + var title = TAPi18n.__(translationKey); + return title !== translationKey ? title : false; + }; + } +}; + +// We automatically close a potential opened popup on any left click on the +// document. To avoid closing it unexpectedly we modify the bubbled event in +// case the click event happen in the popup or in a button that open a popup. +$(document).on('click', function(evt) { + if (evt.which === 1 && ! (evt.originalEvent && + evt.originalEvent.clickInPopup)) { + Popup.close(); + } +}); diff --git a/client/lib/utils.js b/client/lib/utils.js new file mode 100644 index 00000000..9e92e999 --- /dev/null +++ b/client/lib/utils.js @@ -0,0 +1,96 @@ +Utils = { + error: function(err) { + Session.set('error', (err && err.message || false)); + }, + + // scroll + Scroll: function(selector) { + var $el = $(selector); + return { + top: function(px, add) { + var t = $el.scrollTop(); + $el.animate({ scrollTop: (add ? (t + px) : px) }); + }, + left: function(px, add) { + var l = $el.scrollLeft(); + $el.animate({ scrollLeft: (add ? (l + px) : px) }); + } + }; + }, + + Warning: { + get: function() { + return Session.get('warning'); + }, + open: function(desc) { + Session.set('warning', { desc: desc }); + }, + close: function() { + Session.set('warning', false); + } + }, + + // XXX We should remove these two methods + goBoardId: function(_id) { + var board = Boards.findOne(_id); + return board && Router.go('Board', { + _id: board._id, + slug: board.slug + }); + }, + + goCardId: function(_id) { + var card = Cards.findOne(_id); + var board = Boards.findOne(card.boardId); + return board && Router.go('Card', { + cardId: card._id, + boardId: board._id, + slug: board.slug + }); + }, + + liveEvent: function(events, callback) { + $(document).on(events, function() { + callback($(this)); + }); + }, + + capitalize: function(string) { + return string.charAt(0).toUpperCase() + string.slice(1); + }, + + getLabelIndex: function(boardId, labelId) { + var board = Boards.findOne(boardId); + var labels = {}; + _.each(board.labels, function(a, b) { + labels[a._id] = b; + }); + return { + index: labels[labelId], + key: function(key) { + return 'labels.' + labels[labelId] + '.' + key; + } + }; + }, + + // Determine the new sort index + getSortIndex: function(prevCardDomElement, nextCardDomElement) { + // If we drop the card to an empty column + if (! prevCardDomElement && ! nextCardDomElement) { + return 0; + // If we drop the card in the first position + } else if (! prevCardDomElement) { + return Blaze.getData(nextCardDomElement).sort - 1; + // If we drop the card in the last position + } else if (! nextCardDomElement) { + return Blaze.getData(prevCardDomElement).sort + 1; + } + // In the general case take the average of the previous and next element + // sort indexes. + else { + var prevSortIndex = Blaze.getData(prevCardDomElement).sort; + var nextSortIndex = Blaze.getData(nextCardDomElement).sort; + return (prevSortIndex + nextSortIndex) / 2; + } + } +}; diff --git a/client/styles/cheat.styl b/client/styles/cheat.styl new file mode 100644 index 00000000..9d881b44 --- /dev/null +++ b/client/styles/cheat.styl @@ -0,0 +1,79 @@ +@import 'nib' + +.clear + clear: both + +.clearfix + clearfix() + +.hide + display: none + +.show + display: block + +.bold + font-weight: 700 + +.center + text-align: center + +.left + float: left + +.right + float: right + +.first + margin-left: 0 + padding-left: 0 + +.last + margin-right: 0 + padding-right: 0 + +.top + margin-top: 0 + padding-top: 0 + +.bottom + margin-bottom: 0 + padding-bottom: 0 + +.relative + position: relative + +.block + display: block + +.inline + display: inline + +.inline-block + display: inline-block + +.pointer + cursor: pointer + +.ellip + overflow: hidden + text-overflow: ellipsis + white-space: nowrap + +.underline + text-decoration: underline + +.lowercase + text-transform: lowercase + +.invisible + visibility: hidden + +.wrapword + word-wrap: break-word + +.grab + cursor: grab + +.grabbing + cursor: grabbing diff --git a/client/styles/fancy-scrollbar.styl b/client/styles/fancy-scrollbar.styl new file mode 100644 index 00000000..c7a30018 --- /dev/null +++ b/client/styles/fancy-scrollbar.styl @@ -0,0 +1,45 @@ +.fancy-scrollbar + -webkit-overflow-scrolling: touch + + .fancy-scrollbar::-webkit-scrollbar + height: 9px + width: 9px + + &::-webkit-scrollbar-button:start:decrement, + &::-webkit-scrollbar-button:end:increment + background: transparent + display: none + + &::-webkit-scrollbar-track-piece + background: #dbdbdb + + &:vertical:start + border-top-left-radius: 5px + border-top-right-radius: 5px + border-bottom-right-radius: 0 + border-bottom-left-radius: 0 + + &:vertical:end + border-top-left-radius: 0 + border-top-right-radius: 0 + border-bottom-right-radius: 5px + border-bottom-left-radius: 5px + + &:horizontal:start + border-top-left-radius: 5px + border-top-right-radius: 0 + border-bottom-right-radius: 0 + border-bottom-left-radius: 5px + + &:horizontal:end + border-top-left-radius: 0 + border-top-right-radius: 5px + border-bottom-right-radius: 5px + border-bottom-left-radius: 0 + + &::-webkit-scrollbar-thumb:vertical, + &::-webkit-scrollbar-thumb:horizontal + background: #c2c2c2 + border-radius: 5px + display: block + height: 50px diff --git a/client/styles/main.styl b/client/styles/main.styl new file mode 100644 index 00000000..0f12e35e --- /dev/null +++ b/client/styles/main.styl @@ -0,0 +1,814 @@ +@import 'nib' + +html, body, input, select, textarea, button + font: 14px "Helvetica Neue", Arial, Helvetica, sans-serif + line-height: 18px + color: #4d4d4d + +html + font-size: 100% + -webkit-text-size-adjust: 100% + +p + margin: 0 + +ol, +ul + list-style: none + margin: 0 + padding: 0 + +blockquote, q + quotes: none + + &:before, + &:after + content: none + +ins + text-decoration: none + +del + text-decoration: line-through + +table + border-collapse: collapse + border-spacing: 0 + width: 100% + +hr + height: 1px + border: 0 + border: none + width: 100% + background: #dbdbdb + color: #dbdbdb + margin: 15px 0 + padding: 0 + +article, +aside, +figure, +footer, +header, +hgroup, +nav, +section + display: block + +caption, th, td + text-align: left + font-weight: 400 + +a img + border: none + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section, +summary + display: block + +html + max-height: 100% + +body + background: darken(white, 10%) + margin: 0 + position: relative + z-index: 0 + overflow-y: auto + +#surface + display: flex + flex-direction: column + min-height: 100vh + +#content + position: relative + flex: 1 + +div::selection + background: transparent + +h1 + font-size: 22px + line-height: 1.2em + margin: 0 0 10px + +h2 + font-size: 18px + line-height: 1.2em + margin: 0 0 9px + +h3, h4, h5, h6 + font-size: 16px + line-height: 1.25em + margin: 0 0 6px + +.quiet, .quiet a + color: #8c8c8c + +.error, .error a + color: #eb3800 + +.warning + background: #f0ecdb + border-radius: 3px + color: #aa8f09 + padding: 6px 8px + + a + color: #aa8f09 + +a + color: #444 + cursor: pointer + text-decoration: none + + &:hover + color: #111 + + &.disabled, + &.disabled:hover + color: #8c8c8c + cursor: default + text-decoration: none + +table, p + margin-bottom: 8px + +pre + margin: 15px 0 + white-space: pre + max-height: 516px + +pre, +code, +tt + font-family: bitstream vera sans mono, andale mono, lucida console, monospace + line-height: 1.25em + +blockquote + margin: 8px 0 8px 8px + border-left: 1px solid #ccc + color: #666 + padding: 0 0 0 8px + +table, td, th + vertical-align: top + border-top: 1px solid #ccc + border-left: 1px solid #ccc + +td, th + padding: 5px + border-right: 1px solid #ccc + border-bottom: 1px solid #ccc + +th + font-weight: 700 + +thead + background: #fff + background: linear-gradient(to bottom, #fff 0, #f0f0f0 100%) + +tbody + background-color: #fff + +dl, dt + margin-bottom: 8px + +dd + margin: 0 0 16px 24px + +.emoji + height: 18px + width: 18px + vertical-align: text-bottom + +.edit + display: none + position: relative + +.editable .current + cursor: pointer + +.editable.editing + cursor: auto + +.edits-warning, .edits-error + display: none + clear: both + +.editing .edit + display: block + float: left + padding-bottom: 9px + z-index: 100 + width: 100% + +.editing .edits-warning + display: none!important + +.editing .edit .field, +.editing .edit .field:active + background: rgba(0, 0, 0, .03) + box-shadow: inset 0 1px 6px rgba(0, 0, 0, .1) + border-color: rgba(0, 0, 0, .15) + margin-bottom: 4px + +.edit-heavy .field + font-size: 15px + font-weight: 700 + line-height: 18px + + +.board-backgrounds-list + + .board-background-select + box-sizing: border-box + display: block + float: left + width: 50% + padding-top: 12px + position: relative + z-index: 1 + + &:nth-child(-n + 2) + padding-top: 0 + + &:nth-child(2n) + padding-left: 6px + + &:nth-child(2n+1) + padding-right: 6px + + .background-box + border-radius: 3px + background-size: cover + display: block + height: 74px + position: relative + width: 100% + cursor: pointer + display: flex + align-items: center + justify-content: center + + i.fa-check + font-size: 25px + color: white + +.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 + +.editing-members + float: right + + .edit-in-progress + display: inline-block + border: 1px solid #ccc + background: #ddd + margin: 0 4px + border-radius: 2px + + .inline-member + cursor: default + + .inline-member-av + width: 18px + height: 18px + margin: 0 0 -4px 0 + + .initials + margin-left: 3px + + .icon + animation: pulsate 1s ease-in alternate + animation-iteration-count: infinite + +@keyframes pulsate + 0% + opacity: 1 + + to + opacity: .4 + +.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 + +.chrome .minicard.ui-sortable-helper, +.safari .minicard.ui-sortable-helper + box-shadow: -2px 2px 6px rgba(0, 0, 0, .2) + +input[type="text"].attachment-add-link-input + float: left + margin: 0 0 8px + width: 80% + +input[type="submit"].attachment-add-link-submit + float: left + margin: 0 0 8px 4px + padding: 6px 12px + width: 18% + +.card-detail-badge + background-color: #dbdbdb + border-radius: 3px + color: #737373 + cursor: default + display: block + height: 20px + line-height: 20px + margin: 0 4px 4px 0 + padding: 5px 10px + text-align: center + text-decoration: none + + &:hover + color: #737373 + + &.badge-state-clickable + text-decoration: underline + +.badge-state-clickable:hover + color: #262626 + cursor: pointer + text-decoration: underline + +.card-detail-badge-aging:first-letter + text-transform: uppercase + +.badge + color: #8c8c8c + float: left + height: 18px + margin: 0 3px 3px 0 + padding: 0 4px 0 0 + position: relative + text-decoration: none + +.badge-icon + float: left + +.badge-text + float: left + font-size: 12px + +.badge-state-image-only + padding: 0 + + .badge-icon + margin-right: 0 + +.badge-state-clickable + cursor: pointer + + .badge-text + text-decoration: underline + +.badge-state-complete + background-color: #4aba12 + border-radius: 3px + color: #fff + + .badge-icon + color: #fff + +.badge-state-unread-notification + background-color: #990f0f + border-radius: 3px + color: #fff + + .badge-icon + color: #fff + +.badge-state-voted + background-color: #dbdbdb + border-radius: 3px + color: #8c8c8c + + .badge-icon + color: #999 + +.badge-state-due-soon, .badge-state-due-soon:hover + background-color: #e6bf00 + border-radius: 3px + color: #fff + + .badge-icon + color: #fff + +.badge-state-due-now, .badge-state-due-now:hover + background-color: #990f0f + border-radius: 3px + color: #fff + + .badge-icon + color: #fff + +.badge-state-due-past, .badge-state-due-past:hover + background-color: #ad8585 + border-radius: 3px + color: #fff + + .badge-icon + color: #fff + +.checklist-list:empty + display: none + +.checklist + margin-bottom: 16px + +.checklist.placeholder + background: #dcdcdc + border-radius: 3px + +.checklist.ui-sortable-helper + background: rgba(240, 240, 240, .85) + border-radius: 3px + + .checklist-title, + .current, + .window-module-title + cursor: grabbing + + .icon-menu + visibility: hidden + +.checklist-items-list + min-height: 2px + +.checklist-item + clear: both + margin: 0 0 6px + padding: 0 0 4px 38px + position: relative + transform-origin: left bottom + transition-property: transform, opacity, height, padding, margin + transition-duration: .14s + transition-timing-function: ease-in + + &.placeholder + background: #dcdcdc + border-radius: 3px + margin: -5px -5px 5px 5px + padding: 5px 0 + + &.ui-sortable-helper + background: rgba(240, 240, 240, .85) + border-radius: 3px + margin: -3px -3px -3px 7px + padding: 3px 3px 3px 33px + + .checklist-item-checkbox + top: 2px + left: 2px + +.hide-completed-items .checklist-item-fade-out + height: 0 + margin: 0 + opacity: 0 + padding: 0 + transform: rotate(-5deg) translateX(-10px) translateY(-10px) + +.checklist-item-checkbox + background: #fff + border-radius: 3px + box-shadow: 0 2px 3px rgba(0, 0, 0, .1) + border: 1px solid #ccc + border-bottom-color: #b3b3b3 + font-weight: 700 + position: absolute + left: 6px + line-height: 18px + overflow: hidden + text-align: center + text-indent: 100% + top: -2px + height: 18px + width: 18px + white-space: nowrap + + &.enabled:hover + background-color: #f0f0f0 + border-color: #ccc + box-shadow: 0 1px 2px rgba(0, 0, 0, .1) + color: #8c8c8c + cursor: pointer + text-indent: 0 + + &.enabled:active + background-color: #e3e3e3 + border-color: #ccc + box-shadow: inset 0 3px 6px rgba(0, 0, 0, .1) + color: #4d4d4d + text-indent: 0 + +.checklist-item-details-text + min-height: 18px + margin-bottom: 0 + + &.enabled:hover + color: #4d4d4d + cursor: pointer + + &:empty + content: "No name" + color: #8c8c8c + +.checklist-item-state-complete + + .checklist-item-details-text + color: #8c8c8c + font-style: italic + text-decoration: line-through + + img + opacity: .3 + + .checklist-item-checkbox + background-color: #f0f0f0 + border-color: #dbdbdb + border-bottom-color: #ccc + box-shadow: none + text-indent: 0 + + &.enabled:hover + background-color: #e6e6e6 + border-color: #ccc + box-shadow: none + + &.enabled:active + background-color: #dbdbdb + box-shadow: inset 0 3px 6px rgba(0, 0, 0, .1) + +.hide-completed-items .checklist-item-state-complete + display: none + +.checklist-new-item-text, +.checklist-new-item-text:hover + background: transparent + border-color: transparent + box-shadow: none + color: #8c8c8c + cursor: pointer + margin-bottom: 4px + max-height: 32px + overflow: hidden + resize: none + text-decoration: none + + .checklist-new-item.focus & + background: #fff + border-color: #2b7cab + box-shadow: 0 0 3px #2b7cab + color: #4d4d4d + cursor: text + max-height: none + resize: vertical + +.checklist-progress + margin-bottom: 12px + position: relative + +.checklist-progress-percentage + color: #8c8c8c + font-size: 11px + line-height: 10px + position: absolute + left: 0 + top: -1px + text-align: center + width: 38px + +.checklist-progress-bar + background: #dbdbdb + border-radius: 3px + clear: both + height: 8px + margin: 0 0 0 38px + overflow: hidden + position: relative + +.checklist-progress-bar-current + background: #479fd1 + background: linear-gradient(to bottom, #479fd1 0, #2288c3 100%) + bottom: 0 + left: 0 + position: absolute + top: 0 + transition: width .14s ease-in, background .14s ease-in + +.checklist-progress-bar-current-complete + background: #24a828 + +.checklist-completed-text + display: block + margin: 8px 0 0 38px + +.checklist .edit + clear: both + margin-top: -5px + +.explorer .av-btn + background: url(about:blank) + +.atMention + background: #dbdbdb + border-radius: 3px + padding: 1px 4px + margin: -1px 0 + display: inline-block + + &.me + background: #cfdfe8 + +.helper + background-color: #e6e6e6 + border-radius: 3px + color: #8c8c8c + font-size: 13px + line-height: 15px + margin: 4px 0 0 + padding: 6px 8px + width: auto + + a + color: #8c8c8c + + &:hover + color: #666 + +.empty-list, .empty + background: #e6e6e6 + border: 1px dashed #ccc + border-radius: 3px + color: #8c8c8c + display: block + padding: 6px + text-align: center + +.empty-list + border-radius: 6px + padding: 25px 6px + +.search-results-page-contents .empty-list + margin: 12px 0 0 52px + +.window-module .empty-list + margin: 8px 0 0 38px + +.loading + margin: 19px auto + text-align: center + +.big-message + display: block + margin: 75px auto + text-align: center + max-width: 600px + + h1 + font-size: 26px + margin-bottom: 24px + + p + font-size: 18px + line-height: 22px + + &.with-picture + margin-top: 35px + + h1 + margin-top: 20px + + .callout + margin: 20px 0 + +.callout + background: #e3e3e3 + border-radius: 5px + padding: 20px + + ol + text-align: left + list-style-type: decimal + margin-left: 25px + font-size: 16px + + li + margin: 10px 0 + +.gutter + margin-left: 38px diff --git a/client/styles/temp.styl b/client/styles/temp.styl new file mode 100644 index 00000000..9dab7802 --- /dev/null +++ b/client/styles/temp.styl @@ -0,0 +1,110 @@ +/** + * We should merge these declarations in the appropriate stylus files. + */ + +.dn { + display:none; +} + +.header-btn-btn { + padding-left:23px!important; +} + +.bgnone { + background:none!important; +} + +.tac { + text-align:center; + + h1 { + font-size: 2em; + } +} + +.tdn { + text-decoration:none; +} + +.header-member { + min-width:105px!important; + text-align:center; +} + +.primarys { + font-size:20px; + line-height: 1.44em; + padding: .6em 1.3em!important; + border-radius: 3px!important; + box-shadow: 0 2px 0 #4d4d4d!important; +} + +.layout-twothirds-center { + display: block; + max-width: 585px; + margin: 0 auto; + position: relative; + font-size:20px; + line-height: 100px; +} + +#WindowTitleEdit .single-line, .single-line2 { + overflow: hidden; + word-wrap: break-word; + resize: none; + height: 60px; +} + +.single-line2 { + overflow: hidden; + word-wrap: break-word; + resize: none; + height: 108px; +} + +#header-search { + float: left; + margin: 1px 8px 0 0; + position: relative; + z-index: 1; + + label { + display:none; + } + input[type="text"] { + background:rgba(255,255,255,0.5); + border-top-left-radius:3px; + border-top-right-radius:0; + border-bottom-right-radius:0; + border-bottom-left-radius:3px; + border:none; + float:left; + font-size:13px; + height:29px; + min-height:29px; + line-height:19px; + width:160px; + margin:0; + + &:hover{ + background:rgba(255,255,255,0.7); + } + + &:focus{ + background:#e8ebee; + -webkit-box-shadow:none; + box-shadow:none + } + } + + .header-btn{ + border-top-left-radius:0; + border-top-right-radius:3px; + border-bottom-right-radius:3px; + border-bottom-left-radius:0 + } + + input[type="submit"]{ + display:none + } +} |