Renaissance

_,,ad8888888888bba,_
                  ,ad88888I888888888888888ba,
                ,88888888I88888888888888888888a,
              ,d888888888I8888888888888888888888b,
             d88888PP"""" ""YY88888888888888888888b,
           ,d88"'__,,--------,,,,.;ZZZY8888888888888,
          ,8IIl'"                ;;l"ZZZIII8888888888,
         ,I88l;'                  ;lZZZZZ888III8888888,
       ,II88Zl;.                  ;llZZZZZ888888I888888,
      ,II888Zl;.                .;;;;;lllZZZ888888I8888b
     ,II8888Z;;                 `;;;;;''llZZ8888888I8888,
     II88888Z;'                        .;lZZZ8888888I888b
     II88888Z; _,aaa,      .,aaaaa,__.l;llZZZ88888888I888
     II88888IZZZZZZZZZ,  .ZZZZZZZZZZZZZZ;llZZ88888888I888,
     II88888IZZ<'(@@>Z|  |ZZZ<'(@@>ZZZZ;;llZZ888888888I88I
    ,II88888;   `""" ;|  |ZZ; `"""     ;;llZ8888888888I888
    II888888l            `;;          .;llZZ8888888888I888,
   ,II888888Z;           ;;;        .;;llZZZ8888888888I888I
   III888888Zl;    ..,   `;;       ,;;lllZZZ88888888888I888
   II88888888Z;;...;(_    _)      ,;;;llZZZZ88888888888I888,
   II88888888Zl;;;;;' `--'Z;.   .,;;;;llZZZZ88888888888I888b
   ]I888888888Z;;;;'   ";llllll;..;;;lllZZZZ88888888888I8888,
   II888888888Zl.;;"Y88bd888P";;,..;lllZZZZZ88888888888I8888I
   II8888888888Zl;.; `"PPP";;;,..;lllZZZZZZZ88888888888I88888
   II888888888888Zl;;. `;;;l;;;;lllZZZZZZZZW88888888888I88888
   `II8888888888888Zl;.    ,;;lllZZZZZZZZWMZ88888888888I88888
    II8888888888888888ZbaalllZZZZZZZZZWWMZZZ8888888888I888888,
    `II88888888888888888b"WWZZZZZWWWMMZZZZZZI888888888I888888b
     `II88888888888888888;ZZMMMMMMZZZZZZZZllI888888888I8888888
      `II8888888888888888 `;lZZZZZZZZZZZlllll888888888I8888888,
       II8888888888888888, `;lllZZZZllllll;;.Y88888888I8888888b,
      ,II8888888888888888b   .;;lllllll;;;.;..88888888I88888888b,
      II888888888888888PZI;.  .`;;;.;;;..; ...88888888I8888888888,
      II888888888888PZ;;';;.   ;. .;.  .;. .. Y8888888I88888888888b,
     ,II888888888PZ;;'                        `8888888I8888888888888b,
     II888888888'                              888888I8888888888888888
    ,II888888888                              ,888888I8888888888888888
   ,d88888888888                              d888888I8888888888ZZZZZZ
,ad888888888888I                              8888888I8888ZZZZZZZZZZZZ
888888888888888'                              888888IZZZZZZZZZZZZZZZZZ
8888888888P'8P'                               Y888ZZZZZZZZZZZZZZZZZZZZ
888888888,  "                                 ,ZZZZZZZZZZZZZZZZZZZZZZZ
8888888888,                                ,ZZZZZZZZZZZZZZZZZZZZZZZZZZ
888888888888a,      _                    ,ZZZZZZZZZZZZZZZZZZZZ88888888
888888888888888ba,_d'                  ,ZZZZZZZZZZZZZZZZZ8888888888888
8888888888888888888888bbbaaa,,,______,ZZZZZZZZZZZZZZZ88888888888888888
88888888888888888888888888888888888ZZZZZZZZZZZZZZZ88888888888888888888
8888888888888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888888
888888888888888888888888888888888ZZZZZZZZZZZZZZ88888888888888888888888
8888888888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888888888
88888888888888888888888888888ZZZZZZZZZZZZZZ888888888888888888888888888
8888888888888888888888888888ZZZZZZZZZZZZZZ88888888888888888 Normand  8
88888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888 Veilleux 8
8888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888888888888888
This commit is contained in:
Maxime Quandalle 2015-05-12 19:20:58 +02:00
commit 2dbea30842
128 changed files with 10521 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
*~
*.swp
.meteor-spk
.tx/
*.sublime-workspace

77
.jscsrc Normal file
View file

@ -0,0 +1,77 @@
{
"disallowSpacesInNamedFunctionExpression": {
"beforeOpeningRoundBrace": true
},
"disallowSpacesInFunctionExpression": {
"beforeOpeningRoundBrace": true
},
"disallowSpacesInAnonymousFunctionExpression": {
"beforeOpeningRoundBrace": true
},
"disallowSpacesInFunctionDeclaration": {
"beforeOpeningRoundBrace": true
},
"disallowEmptyBlocks": true,
"disallowSpacesInsideArrayBrackets": true,
"disallowSpacesInsideParentheses": true,
"disallowQuotedKeysInObjects": "allButReserved",
"disallowSpaceAfterObjectKeys": true,
"disallowSpaceAfterPrefixUnaryOperators": [
"++",
"--",
"+",
"-",
"~"
],
"disallowSpaceBeforePostfixUnaryOperators": true,
"disallowSpaceBeforeBinaryOperators": [
","
],
"disallowMixedSpacesAndTabs": true,
"disallowTrailingWhitespace": true,
"disallowTrailingComma": true,
"disallowYodaConditions": true,
"disallowKeywords": [ "with" ],
"disallowMultipleLineBreaks": true,
"disallowMultipleVarDecl": "exceptUndefined",
"requireSpaceBeforeBlockStatements": true,
"requireParenthesesAroundIIFE": true,
"requireSpacesInConditionalExpression": true,
"requireBlocksOnNewline": 1,
"requireCommaBeforeLineBreak": true,
"requireSpaceAfterPrefixUnaryOperators": [
"!"
],
"requireSpaceBeforeBinaryOperators": true,
"requireSpaceAfterBinaryOperators": true,
"requireCamelCaseOrUpperCaseIdentifiers": true,
"requireLineFeedAtFileEnd": true,
"requireCapitalizedConstructors": true,
"requireDotNotation": true,
"requireSpacesInForStatement": true,
"requireSpaceBetweenArguments": true,
"requireCurlyBraces": [
"do"
],
"requireSpaceAfterKeywords": [
"if",
"else",
"for",
"while",
"do",
"switch",
"case",
"return",
"try",
"catch",
"typeof"
],
"safeContextKeyword": [
"self",
"view"
],
"validateLineBreaks": "LF",
"validateQuoteMarks": "'",
"validateIndentation": 2,
"maximumLineLength": 80
}

82
.jshintrc Normal file
View file

@ -0,0 +1,82 @@
{
// JSHint options: http://jshint.com/docs/options/
"maxerr": 50,
// Enforcing
"camelcase": true,
"eqeqeq": true,
"undef": true,
"unused": true,
// Environments
"browser": true,
"devel": true,
// Authorized globals
"globals": {
// Meteor globals
"Meteor": false,
"DDP": false,
"Mongo": false,
"Session": false,
"Accounts": false,
"Template": false,
"Blaze": false,
"UI": false,
"Match": false,
"check": false,
"Tracker": false,
"Deps": false,
"ReactiveVar": false,
"EJSON": false,
"HTTP": false,
"Email": false,
"Assets": false,
"Handlebars": false,
"Package": false,
"App": false,
"Npm": false,
"Tinytest": false,
"Random": false,
"HTML": false,
// Exported by packages we use
"_": false,
"$": false,
"Router": false,
"SimpleSchema": false,
"getSlug": false,
"Migrations": false,
"FS": false,
"BlazeComponent": false,
"TAPi18n": false,
"T9n": false,
"SubsManager": false,
"Mousetrap": false,
"Avatar": true,
// Our collections
"Boards": true,
"Lists": true,
"Cards": true,
"CardComments": true,
"Activities": true,
"Attachments": true,
"Users": true,
"AccountsTemplates": true,
// Our objects
"Utils": true,
"Popup": true,
"Filter": true,
"Sidebar": true,
"Mixins": true,
// XXX Temp, we should remove these
"allowIsBoardAdmin": true,
"allowIsBoardMember": true,
"BoardSubsManager": true,
"currentlyOpenedForm": true,
"Emoji": true
}
}

View file

@ -0,0 +1,8 @@
# This file contains information which helps Meteor properly upgrade your
# app when you run 'meteor update'. You should check it into version control
# with your project.
notices-for-0.9.0
notices-for-0.9.1
0.9.4-platform-file
notices-for-facebook-graph-api-2

1
.meteor/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
local

7
.meteor/.id Normal file
View file

@ -0,0 +1,7 @@
# This file contains a token that is unique to your project.
# Check it into your repository along with the rest of this directory.
# It can be used for purposes such as:
# - ensuring you don't accidentally deploy one app on top of another
# - providing package authors with aggregated statistics
dvyihgykyzec6y1dpg

1
.meteor/cordova-plugins Normal file
View file

@ -0,0 +1 @@

53
.meteor/packages Normal file
View file

@ -0,0 +1,53 @@
# Meteor packages used by this project, one per line.
#
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
meteor-platform
# Account system
accounts-password
kenton:accounts-sandstorm
service-configuration
useraccounts:unstyled
# Compilers
mquandalle:jade
mquandalle:stylus
# Collections
aldeed:collection2
cfs:gridfs
cfs:standard-packages
dburles:collection-helpers
idmontie:migrations
matb33:collection-hooks
matteodem:easy-search
reywood:publish-composite
# Utilities
alethes:pages
audit-argument-checks
iron:router
meteorhacks:subs-manager
mquandalle:autofocus
mquandalle:moment
ongoworks:speakingurl
raix:handlebar-helpers
random
reactive-dict
tap:i18n
tmeasday:presence
underscore
# UI components
bengott:avatar
fortawesome:fontawesome
linto:jquery-ui
markdown
mousetrap:mousetrap
mquandalle:jquery-textcomplete
peerlibrary:blaze-components
reactive-var
seriousm:emoji-continued
useraccounts:core

2
.meteor/platforms Normal file
View file

@ -0,0 +1,2 @@
server
browser

1
.meteor/release Normal file
View file

@ -0,0 +1 @@
METEOR@1.1.0.2

120
.meteor/versions Normal file
View file

@ -0,0 +1,120 @@
accounts-base@1.2.0
accounts-password@1.1.1
aldeed:collection2@2.3.3
aldeed:simple-schema@1.3.3
alethes:pages@1.8.4
audit-argument-checks@1.0.3
autoupdate@1.2.1
base64@1.0.3
bengott:avatar@0.7.6
binary-heap@1.0.3
blaze@2.1.2
blaze-tools@1.0.3
boilerplate-generator@1.0.3
callback-hook@1.0.3
cfs:access-point@0.1.49
cfs:base-package@0.0.30
cfs:collection@0.5.5
cfs:collection-filters@0.2.4
cfs:data-man@0.0.6
cfs:file@0.1.17
cfs:gridfs@0.0.33
cfs:http-methods@0.0.29
cfs:http-publish@0.0.13
cfs:power-queue@0.9.11
cfs:reactive-list@0.0.9
cfs:reactive-property@0.0.4
cfs:standard-packages@0.5.9
cfs:storage-adapter@0.2.2
cfs:tempstore@0.1.5
cfs:upload-http@0.0.20
cfs:worker@0.1.4
check@1.0.5
coffeescript@1.0.6
dburles:collection-helpers@1.0.3
ddp@1.1.0
deps@1.0.7
ejson@1.0.6
email@1.0.6
fastclick@1.0.3
fortawesome:fontawesome@4.3.0
geojson-utils@1.0.3
html-tools@1.0.4
htmljs@1.0.4
http@1.1.0
id-map@1.0.3
idmontie:migrations@1.0.0
iron:controller@1.0.7
iron:core@1.0.7
iron:dynamic-template@1.0.7
iron:layout@1.0.7
iron:location@1.0.7
iron:middleware-stack@1.0.7
iron:router@1.0.7
iron:url@1.0.7
jparker:crypto-core@0.1.0
jparker:crypto-md5@0.1.1
jparker:gravatar@0.3.1
jquery@1.11.3_2
json@1.0.3
kenton:accounts-sandstorm@0.1.3
launch-screen@1.0.2
less@1.0.14
linto:jquery-ui@1.11.2
livedata@1.0.13
localstorage@1.0.3
logging@1.0.7
markdown@1.0.4
matb33:collection-hooks@0.7.13
matteodem:easy-search@1.5.6
meteor@1.1.6
meteor-platform@1.2.2
meteorhacks:subs-manager@1.3.0
minifiers@1.1.5
minimongo@1.0.8
mobile-status-bar@1.0.3
mongo@1.1.0
mongo-livedata@1.0.8
mousetrap:mousetrap@1.4.6_1
mquandalle:autofocus@1.0.0
mquandalle:jade@0.4.3
mquandalle:jade-compiler@0.4.3
mquandalle:jquery-textcomplete@0.3.6_1
mquandalle:moment@1.0.0
mquandalle:stylus@1.1.1
npm-bcrypt@0.7.8_2
observe-sequence@1.0.6
ongoworks:speakingurl@1.1.0
ordered-dict@1.0.3
peerlibrary:assert@0.2.5
peerlibrary:base-component@0.8.0
peerlibrary:blaze-components@0.10.0
raix:eventemitter@0.1.2
raix:handlebar-helpers@0.2.4
random@1.0.3
reactive-dict@1.1.0
reactive-var@1.0.5
reload@1.1.3
retry@1.0.3
reywood:publish-composite@1.3.6
routepolicy@1.0.5
seriousm:emoji-continued@1.4.0
service-configuration@1.0.4
session@1.1.0
sha@1.0.3
softwarerero:accounts-t9n@1.0.9
spacebars@1.0.6
spacebars-compiler@1.0.6
srp@1.0.3
stylus@1.0.7
tap:i18n@1.4.1
templating@1.1.1
tmeasday:presence@1.0.6
tracker@1.0.7
ui@1.0.6
underscore@1.0.3
url@1.0.4
useraccounts:core@1.9.1
useraccounts:unstyled@1.9.1
webapp@1.2.0
webapp-hashing@1.0.3

7
.travis.yml Normal file
View file

@ -0,0 +1,7 @@
language: node_js
node_js:
- "0.10"
before_install:
- "curl -L http://git.io/ejPSng | /bin/sh"
services:
- mongodb

57
Contributing.md Normal file
View file

@ -0,0 +1,57 @@
# Contributing
Were glad youre interested in helping the LibreBoard project! We welcome bug
reports, enhancement ideas, and pull requests, in our GitHub bug tracker. Before
opening a new thread please verify that your issue hasnt already been reported.
<https://github.com/libreboard/libreboard>
## Translations
You are encouraged to translate (or improve the translation of) LibreBoard in
your locale language. For that purpose we rely on
[Transifex](https://www.transifex.com/projects/p/libreboard). So the first step
is to create a Transifex account if you dont have one already. You can then
send a request to join one of the translation teams. If there we will create a
new one.
Once you are in a team you can start translating the application. Please take a
look at the glossary so you can agree with other (present and future)
contributors on words to use to translate key concepts in the application like
“boards” and “cards”.
The original application is written in English, and if you want to contribute to
the application itself, you are asked to fill the `i18n/en.i18n.json` file. When
you do that the new strings of text to translate automatically appears on
Transifex to be translated (the refresh may take a few hours).
We pull all translations from Transifex before every new LibreBoard release
candidate, ask the translators to review the app, and pull all translations
again for the final release.
## Installation
LibreBoard is made with [Meteor](https://www.meteor.com). Thus the easiest way
to start hacking is by installing the framework, cloning the git repository, and
launching the application:
```bash
$ curl https://install.meteor.com/ | sh # On Mac or Linux
$ git clone https://github.com/libreboard/libreboard.git
$ cd libreboard
$ meteor
```
As for any Meteor application, LibreBoard is automatically refreshed when you
change any file of the source code, just play with it to see how it behaves!
## Style guide
We follow the
[meteor style guide](https://github.com/meteor/meteor/wiki/Meteor-Style-Guide).
Please read the meteor style guide before making any significant contribution.
## Code organisation
TODO

9
Dockerfile Normal file
View file

@ -0,0 +1,9 @@
FROM meteorhacks/meteord
MAINTAINER Maxime Quandalle <maxime@quandalle.com>
# Run as you wish!
#
# sudo docker run -d \
# -e "ROOT_URL=http://example.com"
# -e "MONGO_URL=mongodb://172.17.0.3:27017/libreboard-test" \
# -p 8080:80

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014-2015 Yasar Icli, Maxime Quandalle
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

25
README.md Normal file
View file

@ -0,0 +1,25 @@
# LibreBoard [![Build Status][travis-status]][travis-link]
LibreBoard is an open-source *kanban* board that let you organize things in
cards, and cards in lists. You can use it alone, or with your team and family
thanks to our real-time synchronisation feature. Libreboard is a land of liberty
and you can implement all sort of workflows on it using tags, comments, member
assignation, and many more.
[![Our roadmap is self-hosted on LibreBoard][thumbnail]][roadmap]
Since it is a free software, you dont have to trust us with your data and can
install LibreBoard on your own computer or server. In fact we encourage you to
do that by providing one-click installation for the
[Sandstorm](https://sandstorm.io) platform and verified
[Docker](https://www.docker.com) images.
LibreBoard is released under the very permissive [MIT license](LICENSE), and
made with [Meteor](https://www.meteor.com).
[Our roadmap is self-hosted on LibreBoard][roadmap]
[travis-status]: https://travis-ci.org/libreboard/libreboard.svg
[travis-link]: https://travis-ci.org/libreboard/libreboard.svg
[thumbnail]: http://i.imgur.com/IIdHUmW.png
[roadmap]: http://libreboard.com/boards/MeSsFJaSqeuo9M6bs/libreboard-roadmap

View file

@ -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

View file

@ -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');

View file

View file

@ -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();
}
});

View file

@ -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>

View file

@ -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'}}

View file

@ -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');

View file

@ -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

View file

@ -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)

View file

@ -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();
}
});

View file

@ -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'}}")

View file

@ -0,0 +1,7 @@
Template.headerBoard.helpers({
isStarred: function() {
var boardId = Session.get('currentBoard');
var user = Meteor.user();
return boardId && user && user.hasStarred(boardId);
}
});

View file

@ -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;
}
}
}

View file

@ -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);
}
});

View file

@ -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'}}

View file

@ -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

View file

@ -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);
}
});

View file

@ -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

View file

@ -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
}
});
}
});

View file

@ -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%

View file

@ -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();
}
});

View file

@ -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);
}
});

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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);
}
});

View file

@ -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>

View file

@ -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('');
}
});

View file

@ -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

View file

@ -0,0 +1,6 @@
template(name='inlinedForm')
if isOpen.get
form(id=id class=classNames)
+Template.contentBlock
else
+Template.elseBlock

View file

@ -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');

View file

@ -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

View file

@ -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');

View file

@ -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 = '';
}
}
});

View file

@ -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

View file

@ -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');

View file

@ -0,0 +1,5 @@
template(name='list')
.list.js-list(id="js-list-{{_id}}")
.list-wrapper
+listHeader
+listBody

View file

@ -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');

View file

@ -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

View file

@ -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'}}")

View file

@ -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();
}
});

View file

@ -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();
}
}
});

View file

@ -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

View file

@ -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')
});

View file

@ -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;
// }

View file

@ -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: '&commat;', 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);
}));

View file

@ -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

View file

@ -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);
}
});

View file

@ -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%

View file

@ -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)

View file

@ -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
}
]);
};

View file

@ -0,0 +1,5 @@
Router.route('/', {
name: 'Home',
redirectLoggedInUsers: true,
authenticated: true
});

View file

@ -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) }
}

View file

@ -0,0 +1,6 @@
.sk-spinner.sk-spinner-wave(class=currentBoard.colorClass)
.sk-rect1
.sk-rect2
.sk-rect3
.sk-rect4
.sk-rect5

View file

@ -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>

View file

@ -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();
}
});

View file

View file

@ -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)

View file

@ -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);
// }
// });

View file

@ -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;
// }
// });

View file

@ -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');
}
}
}];
}
});

View file

@ -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();
}
});
});
});
};

View file

@ -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');

View file

@ -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

View file

@ -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(&quot;{{url}}&quot;);">
<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>

View file

@ -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'}}

View file

@ -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)

View file

@ -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')
});

View file

@ -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

View file

@ -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'}}

View file

@ -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')
});

View file

@ -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();
}
});

View file

@ -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

View file

@ -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'
});

View file

@ -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>

35
client/config/accounts.js Normal file
View file

@ -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
}
]);

3
client/config/avatar.js Normal file
View file

@ -0,0 +1,3 @@
Avatar.options = {
fallbackType: 'initials'
};

28
client/config/router.js Normal file
View file

@ -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();
}
});

152
client/lib/emoji-values.js Normal file
View file

@ -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'];

133
client/lib/filter.js Normal file
View file

@ -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);

22
client/lib/i18n.js Normal file
View file

@ -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);
}
});

55
client/lib/keyboard.js Normal file
View file

@ -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);
}
});

1
client/lib/mixins.js Normal file
View file

@ -0,0 +1 @@
Mixins = {};

Some files were not shown because too many files have changed in this diff Show more