mirror of
https://github.com/wekan/wekan.git
synced 2025-04-19 11:44:42 -04:00
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:
commit
2dbea30842
128 changed files with 10521 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
*~
|
||||
*.swp
|
||||
.meteor-spk
|
||||
.tx/
|
||||
*.sublime-workspace
|
77
.jscsrc
Normal file
77
.jscsrc
Normal 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
82
.jshintrc
Normal 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
|
||||
}
|
||||
}
|
8
.meteor/.finished-upgraders
Normal file
8
.meteor/.finished-upgraders
Normal 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
1
.meteor/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
local
|
7
.meteor/.id
Normal file
7
.meteor/.id
Normal 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
1
.meteor/cordova-plugins
Normal file
|
@ -0,0 +1 @@
|
|||
|
53
.meteor/packages
Normal file
53
.meteor/packages
Normal 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
2
.meteor/platforms
Normal file
|
@ -0,0 +1,2 @@
|
|||
server
|
||||
browser
|
1
.meteor/release
Normal file
1
.meteor/release
Normal file
|
@ -0,0 +1 @@
|
|||
METEOR@1.1.0.2
|
120
.meteor/versions
Normal file
120
.meteor/versions
Normal 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
7
.travis.yml
Normal 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
57
Contributing.md
Normal file
|
@ -0,0 +1,57 @@
|
|||
# Contributing
|
||||
|
||||
We’re glad you’re 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 hasn’t 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 don’t 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
9
Dockerfile
Normal 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
21
LICENSE
Normal 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
25
README.md
Normal 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 don’t 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
|
8
client/components/activities/activities.jade
Normal file
8
client/components/activities/activities.jade
Normal 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
|
77
client/components/activities/activities.js
Normal file
77
client/components/activities/activities.js
Normal 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');
|
0
client/components/activities/comments.jade
Normal file
0
client/components/activities/comments.jade
Normal file
0
client/components/activities/comments.js
Normal file
0
client/components/activities/comments.js
Normal file
30
client/components/activities/events.js
Normal file
30
client/components/activities/events.js
Normal 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();
|
||||
}
|
||||
});
|
154
client/components/activities/templates.html
Normal file
154
client/components/activities/templates.html
Normal 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>
|
33
client/components/boards/body.jade
Normal file
33
client/components/boards/body.jade
Normal 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'}}
|
70
client/components/boards/body.js
Normal file
70
client/components/boards/body.js
Normal 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');
|
54
client/components/boards/body.styl
Normal file
54
client/components/boards/body.styl
Normal 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
|
34
client/components/boards/colors.styl
Normal file
34
client/components/boards/colors.styl
Normal 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)
|
96
client/components/boards/events.js
Normal file
96
client/components/boards/events.js
Normal 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();
|
||||
}
|
||||
});
|
87
client/components/boards/header.jade
Normal file
87
client/components/boards/header.jade
Normal 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'}}")
|
7
client/components/boards/header.js
Normal file
7
client/components/boards/header.js
Normal 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);
|
||||
}
|
||||
});
|
137
client/components/boards/header.styl
Normal file
137
client/components/boards/header.styl
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
45
client/components/boards/helpers.js
Normal file
45
client/components/boards/helpers.js
Normal 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);
|
||||
}
|
||||
});
|
14
client/components/boards/list.jade
Normal file
14
client/components/boards/list.jade
Normal 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'}}
|
85
client/components/boards/list.styl
Normal file
85
client/components/boards/list.styl
Normal 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
|
34
client/components/boards/router.js
Normal file
34
client/components/boards/router.js
Normal 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);
|
||||
}
|
||||
});
|
47
client/components/cards/details.jade
Normal file
47
client/components/cards/details.jade
Normal 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
|
103
client/components/cards/details.js
Normal file
103
client/components/cards/details.js
Normal 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
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
161
client/components/cards/details.styl
Normal file
161
client/components/cards/details.styl
Normal 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%
|
285
client/components/cards/events.js
Normal file
285
client/components/cards/events.js
Normal 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();
|
||||
}
|
||||
});
|
48
client/components/cards/helpers.js
Normal file
48
client/components/cards/helpers.js
Normal 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);
|
||||
}
|
||||
});
|
183
client/components/cards/labels.styl
Normal file
183
client/components/cards/labels.styl
Normal 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
|
136
client/components/cards/minicard.styl
Normal file
136
client/components/cards/minicard.styl
Normal 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
|
12
client/components/cards/popups.jade
Normal file
12
client/components/cards/popups.jade
Normal 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
|
15
client/components/cards/router.js
Normal file
15
client/components/cards/router.js
Normal 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);
|
||||
}
|
||||
});
|
336
client/components/cards/templates.html
Normal file
336
client/components/cards/templates.html
Normal 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>
|
22
client/components/forms/cachedValue.js
Normal file
22
client/components/forms/cachedValue.js
Normal 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('');
|
||||
}
|
||||
});
|
636
client/components/forms/forms.styl
Normal file
636
client/components/forms/forms.styl
Normal 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
|
6
client/components/forms/inlinedform.jade
Normal file
6
client/components/forms/inlinedform.jade
Normal file
|
@ -0,0 +1,6 @@
|
|||
template(name='inlinedForm')
|
||||
if isOpen.get
|
||||
form(id=id class=classNames)
|
||||
+Template.contentBlock
|
||||
else
|
||||
+Template.elseBlock
|
93
client/components/forms/inlinedform.js
Normal file
93
client/components/forms/inlinedform.js
Normal 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');
|
50
client/components/lists/body.jade
Normal file
50
client/components/lists/body.jade
Normal 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
|
73
client/components/lists/body.js
Normal file
73
client/components/lists/body.js
Normal 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');
|
16
client/components/lists/events.js
Normal file
16
client/components/lists/events.js
Normal 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 = '';
|
||||
}
|
||||
}
|
||||
});
|
13
client/components/lists/header.jade
Normal file
13
client/components/lists/header.jade
Normal 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
|
25
client/components/lists/header.js
Normal file
25
client/components/lists/header.js
Normal 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');
|
5
client/components/lists/main.jade
Normal file
5
client/components/lists/main.jade
Normal file
|
@ -0,0 +1,5 @@
|
|||
template(name='list')
|
||||
.list.js-list(id="js-list-{{_id}}")
|
||||
.list-wrapper
|
||||
+listHeader
|
||||
+listBody
|
81
client/components/lists/main.js
Normal file
81
client/components/lists/main.js
Normal 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');
|
136
client/components/lists/main.styl
Normal file
136
client/components/lists/main.styl
Normal 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
|
28
client/components/lists/menu.jade
Normal file
28
client/components/lists/menu.jade
Normal 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'}}")
|
46
client/components/lists/menu.js
Normal file
46
client/components/lists/menu.js
Normal 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();
|
||||
}
|
||||
});
|
8
client/components/main/events.js
Normal file
8
client/components/main/events.js
Normal 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();
|
||||
}
|
||||
}
|
||||
});
|
40
client/components/main/header.jade
Normal file
40
client/components/main/header.jade
Normal 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
|
10
client/components/main/header.js
Normal file
10
client/components/main/header.js
Normal 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')
|
||||
});
|
266
client/components/main/header.styl
Normal file
266
client/components/main/header.styl
Normal 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;
|
||||
// }
|
63
client/components/main/helpers.js
Normal file
63
client/components/main/helpers.js
Normal 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: '@', 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);
|
||||
}));
|
17
client/components/main/layouts.jade
Normal file
17
client/components/main/layouts.jade
Normal 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
|
||||
|
16
client/components/main/popup.js
Normal file
16
client/components/main/popup.js
Normal 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);
|
||||
}
|
||||
});
|
585
client/components/main/popup.styl
Normal file
585
client/components/main/popup.styl
Normal 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%
|
13
client/components/main/popup.tpl.jade
Normal file
13
client/components/main/popup.tpl.jade
Normal 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)
|
40
client/components/main/rendered.js
Normal file
40
client/components/main/rendered.js
Normal 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
|
||||
}
|
||||
]);
|
||||
};
|
5
client/components/main/router.js
Normal file
5
client/components/main/router.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
Router.route('/', {
|
||||
name: 'Home',
|
||||
redirectLoggedInUsers: true,
|
||||
authenticated: true
|
||||
});
|
45
client/components/main/spinner.styl
Normal file
45
client/components/main/spinner.styl
Normal 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) }
|
||||
}
|
6
client/components/main/spinner.tpl.jade
Normal file
6
client/components/main/spinner.tpl.jade
Normal file
|
@ -0,0 +1,6 @@
|
|||
.sk-spinner.sk-spinner-wave(class=currentBoard.colorClass)
|
||||
.sk-rect1
|
||||
.sk-rect2
|
||||
.sk-rect3
|
||||
.sk-rect4
|
||||
.sk-rect5
|
18
client/components/main/templates.html
Normal file
18
client/components/main/templates.html
Normal 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>
|
14
client/components/modal/events.js
Normal file
14
client/components/modal/events.js
Normal 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();
|
||||
}
|
||||
});
|
0
client/components/modal/helpers.js
Normal file
0
client/components/modal/helpers.js
Normal file
5
client/components/modal/modal.tpl.jade
Normal file
5
client/components/modal/modal.tpl.jade
Normal 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)
|
93
client/components/sidebar/events.js
Normal file
93
client/components/sidebar/events.js
Normal 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);
|
||||
// }
|
||||
// });
|
51
client/components/sidebar/helpers.js
Normal file
51
client/components/sidebar/helpers.js
Normal 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;
|
||||
// }
|
||||
// });
|
37
client/components/sidebar/infiniteScrolling.js
Normal file
37
client/components/sidebar/infiniteScrolling.js
Normal 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');
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
});
|
21
client/components/sidebar/rendered.js
Normal file
21
client/components/sidebar/rendered.js
Normal 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
55
client/components/sidebar/sidebar.js
Normal file
55
client/components/sidebar/sidebar.js
Normal 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');
|
154
client/components/sidebar/sidebar.styl
Normal file
154
client/components/sidebar/sidebar.styl
Normal 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
|
307
client/components/sidebar/templates.html.old
Normal file
307
client/components/sidebar/templates.html.old
Normal 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("{{url}}");">
|
||||
<a class="background-option js-background-attribution" href={{href}} target="_blank" title={{title}}>
|
||||
<img src="https://d78fikflryjgj.cloudfront.net/images/d906fe5c1274c56c5571d49705547587/cc.png" style="height: 14px; width: 14px; vertical-align: text-top;" title="http://creativecommons.org/licenses/by/2.0/deed.en">
|
||||
<span class="text" style="margin-left: 2px;">{{author}}</span>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
--}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template name="closeBoardPopup">
|
||||
<p>{{_ 'close-board-pop'}}</p>
|
||||
<input type="submit" class="js-confirm negate full" value="{{_ 'close'}}">
|
||||
</template>
|
||||
|
||||
<template name="removeMemberPopup">
|
||||
<p>{{_ 'remove-member-pop'
|
||||
name=user.profile.name
|
||||
username=user.username
|
||||
boardTitle=board.title}}</p>
|
||||
<input type="submit" class="js-confirm negate full" value="{{_ 'remove-member'}}">
|
||||
</template>
|
||||
|
||||
<template name="addMemberPopup">
|
||||
<div class="search-with-spinner">
|
||||
{{> esInput index="users" }}
|
||||
</div>
|
||||
|
||||
<div class="manage-member-section hide js-search-results" style="display: block;">
|
||||
<ul class="pop-over-member-list options js-list">
|
||||
{{# esEach index="users"}}
|
||||
<li class="item js-member-item {{# if isBoardMember }}disabled{{/if}}">
|
||||
<a href="#" class="name js-select-member {{# if isBoardMember }}multi-line{{/if}}" title="{{ profile.name }} ({{ username }})">
|
||||
{{> userAvatar user=this size="small" }}
|
||||
<span class="full-name">
|
||||
{{ profile.name }} (<span class="username">{{ username }}</span>)
|
||||
</span>
|
||||
{{# if isBoardMember }}
|
||||
<div class="extra-text quiet">({{_ 'joined'}})</div>
|
||||
{{/if}}
|
||||
<span class="icon-sm fa fa-chevron-right light option js-open-option"></span>
|
||||
</a>
|
||||
</li>
|
||||
{{/esEach }}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{{# ifEsIsSearching index='users' }}
|
||||
<div class="tac">
|
||||
<span class="tabbed-pane-main-col-loading-spinner spinner"></span>
|
||||
</div>
|
||||
{{ /ifEsIsSearching }}
|
||||
|
||||
{{# ifEsHasNoResults index="users" }}
|
||||
<div class="manage-member-section js-no-results">
|
||||
<p class="quiet center" style="padding: 16px 4px;">{{_ 'no-results'}}</p>
|
||||
</div>
|
||||
{{ /ifEsHasNoResults }}
|
||||
|
||||
<div class="manage-member-section js-helper">
|
||||
<p class="bottom quiet" style="padding: 6px;">{{_ 'search-member-desc'}}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template name="changePermissionsPopup">
|
||||
<ul class="pop-over-list">
|
||||
<li>
|
||||
<a class="{{#if isLastAdmin}}disabled{{else}}js-set-admin{{/if}}">
|
||||
{{_ 'admin'}}
|
||||
{{#if isAdmin}}<span class="icon-sm fa fa-check"></span>{{/if}}
|
||||
<span class="sub-name">{{_ 'admin-desc'}}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="{{#if isLastAdmin}}disabled{{else}}js-set-normal{{/if}}">
|
||||
{{_ 'normal'}}
|
||||
{{#unless isAdmin}}<span class="icon-sm fa fa-check"></span>{{/unless}}
|
||||
<span class="sub-name">{{_ 'normal-desc'}}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
{{#if isLastAdmin}}
|
||||
<hr>
|
||||
<p class="quiet bottom">{{_ 'last-admin-desc'}}</p>
|
||||
{{/if}}
|
||||
</template>
|
103
client/components/sidebar/templates.jade
Normal file
103
client/components/sidebar/templates.jade
Normal 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'}}
|
7
client/components/users/avatar.jade
Normal file
7
client/components/users/avatar.jade
Normal 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)
|
59
client/components/users/events.js
Normal file
59
client/components/users/events.js
Normal 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')
|
||||
});
|
50
client/components/users/form.styl
Normal file
50
client/components/users/form.styl
Normal 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
|
27
client/components/users/headerButtons.jade
Normal file
27
client/components/users/headerButtons.jade
Normal 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'}}
|
5
client/components/users/headerButtons.js
Normal file
5
client/components/users/headerButtons.js
Normal 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')
|
||||
});
|
27
client/components/users/helpers.js
Normal file
27
client/components/users/helpers.js
Normal 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();
|
||||
}
|
||||
});
|
107
client/components/users/member.styl
Normal file
107
client/components/users/member.styl
Normal 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
|
||||
|
29
client/components/users/router.js
Normal file
29
client/components/users/router.js
Normal 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'
|
||||
});
|
118
client/components/users/templates.html
Normal file
118
client/components/users/templates.html
Normal 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
35
client/config/accounts.js
Normal 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
3
client/config/avatar.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
Avatar.options = {
|
||||
fallbackType: 'initials'
|
||||
};
|
28
client/config/router.js
Normal file
28
client/config/router.js
Normal 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
152
client/lib/emoji-values.js
Normal 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
133
client/lib/filter.js
Normal 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
22
client/lib/i18n.js
Normal 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
55
client/lib/keyboard.js
Normal 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
1
client/lib/mixins.js
Normal file
|
@ -0,0 +1 @@
|
|||
Mixins = {};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue