Compare commits

...

30 Commits
1.5.3 ... 1.7.0

Author SHA1 Message Date
Thaum Rystra
058ee2691f Merge branch 'feature-print'
# Conflicts:
#	rpg-docs/package-lock.json
2018-03-03 17:18:16 +02:00
Thaum Rystra
f0cf7f4956 Added QR code and finished page 1 2018-03-03 17:13:36 +02:00
Thaum Rystra
75c8720b04 Moved printed character sheets to their own page
This makes sure the entire printed sheet is rendered before the browser  attempts to print it, solving all manner of errors
2018-03-03 11:13:16 +02:00
Thaum Rystra
f73f2f670f Formatted all existing printed character sheet fields nicely 2018-03-02 21:40:21 +02:00
Thaum Rystra
c6e62e1cfa Added borders to ability scores and AC 2018-03-02 07:25:37 +02:00
Thaum Rystra
67956d9a42 Fixed dropdown boxes being clipped in dialogs, updated Meteor 2018-01-26 18:18:49 +02:00
Stefan Zermatten
64b3ca6066 Added proficiencies to printed sheet; some fixes 2017-12-12 15:54:21 +02:00
Stefan Zermatten
8349f7da9b Merge branch 'master' into feature-print 2017-12-06 09:22:57 +02:00
Stefan Zermatten
0636042878 Added core-js because an error message told me to 2017-11-22 14:34:54 +02:00
Stefan Zermatten
6207ffa516 Updated meteor 2017-11-21 10:53:07 +02:00
Stefan Zermatten
9d33612054 Merge branch 'feature-rate-limiting-api'
closes #141
2017-11-21 08:50:03 +02:00
Stefan Zermatten
face6387a0 Fixed attacks not being added to spells added with multi-add
Fixes #145
2017-11-01 15:55:07 +02:00
Stefan Zermatten
b308595dac Implement both tokens and rate limiting to API
Closes #141, but still needs better UI on failure
2017-10-12 16:24:12 +02:00
Stefan Zermatten
1d2de197a4 Replaced trello with github on home page 2017-09-28 14:04:42 +02:00
Stefan Zermatten
a3d790b47d Merge branch 'feature-manage-user' 2017-09-28 13:47:46 +02:00
Stefan Zermatten
efe6dd87db Added link to change password
closes #78
2017-09-28 13:47:30 +02:00
Stefan Zermatten
5b33a6e783 Fixed username dialog title 2017-09-28 13:22:21 +02:00
Stefan Zermatten
8730fab40b Moved username dialog to own folder 2017-09-28 13:21:14 +02:00
Stefan Zermatten
992776bb40 Merge branch 'feature-onboarding' 2017-09-28 13:18:52 +02:00
Stefan Zermatten
bc9ec4421c Polished onboarding, removed stray logs
closes #102
2017-09-28 13:17:33 +02:00
Stefan Zermatten
4c31ab601c Improved new user experience and fixed errors on character delete 2017-09-28 13:03:54 +02:00
Stefan Zermatten
c4e77c7eae Replaced all paper-tooltip with custom, working version 2017-09-28 11:47:03 +02:00
Stefan Zermatten
2cd6e27f70 Finished basic new user experience 2017-09-28 10:26:45 +02:00
Stefan Zermatten
f6b2dde479 Added basic onboarding steps 2017-09-27 16:19:00 +02:00
Stefan Zermatten
44da62a962 Lowered subscription caching to improve performance 2017-09-26 15:11:03 +02:00
Stefan Zermatten
4e96047e90 Added rate limiting to heavy subscriptions 2017-09-26 14:59:41 +02:00
Stefan Zermatten
212986ac37 Fixed missing charId 2017-09-26 13:54:33 +02:00
Stefan Zermatten
877f516565 Added efficient computation of characters to replace heavy export functionality 2017-09-26 13:36:01 +02:00
Thaum Rystra
750022f0f1 Added missing indices 2017-09-24 04:02:15 +02:00
Stefan Zermatten
00a050d337 Added basic printing functionality 2017-09-11 09:27:01 +02:00
99 changed files with 5810 additions and 247 deletions

View File

@@ -3,9 +3,8 @@
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
iron:router
accounts-password@1.4.0
accounts-ui@1.1.9
accounts-password@1.5.0
accounts-ui@1.2.0
random@1.0.10
dburles:collection-helpers
reactive-var@1.0.11
@@ -18,37 +17,40 @@ dburles:mongo-collection-instances
percolate:migrations
ecwyne:mathjs
useraccounts:polymer
accounts-google@1.2.0
accounts-google@1.3.0
splendido:accounts-meld
email@1.2.3
meteorhacks:subs-manager
chuangbo:marked
reywood:iron-router-ga
meteor-base@1.1.0
mobile-experience@1.0.4
mongo@1.2.0
meteor-base@1.2.0
mobile-experience@1.0.5
mongo@1.3.1
blaze-html-templates
session@1.1.7
jquery@1.11.10
tracker@1.1.3
logging@1.1.17
logging@1.1.19
reload@1.1.11
ejson@1.0.14
ejson@1.1.0
spacebars
check@1.2.5
useraccounts:iron-routing
wizonesolutions:canonical
standard-minifier-js@2.1.1
shell-server@0.2.4
standard-minifier-js@2.2.0
shell-server@0.3.0
seba:minifiers-autoprefixer
nikogosovd:multiple-uihooks
templates:array
ecmascript@0.8.2
ecmascript@0.9.0
es5-shim@4.6.15
differential:vulcanize
reactive-dict@1.1.9
reactive-dict@1.2.0
percolate:synced-cron
ongoworks:speakingurl
service-configuration@1.0.11
google-config-ui
dynamic-import
google-config-ui@1.0.0
dynamic-import@0.2.0
ddp-rate-limiter@1.0.7
rate-limit@1.0.8
iron:router

View File

@@ -1 +1 @@
METEOR@1.5.2
METEOR@1.6.0.1

View File

@@ -1,25 +1,25 @@
accounts-base@1.3.3
accounts-google@1.2.0
accounts-base@1.4.2
accounts-google@1.3.1
accounts-oauth@1.1.15
accounts-password@1.4.0
accounts-ui@1.1.9
accounts-ui-unstyled@1.2.1
accounts-password@1.5.0
accounts-ui@1.2.0
accounts-ui-unstyled@1.3.0
aldeed:collection2@2.10.0
aldeed:collection2-core@1.2.0
aldeed:schema-deny@1.1.0
aldeed:schema-index@1.1.1
aldeed:simple-schema@1.5.3
allow-deny@1.0.9
allow-deny@1.1.0
autoupdate@1.3.12
babel-compiler@6.20.0
babel-runtime@1.0.1
babel-compiler@6.24.7
babel-runtime@1.1.1
base64@1.0.10
binary-heap@1.0.10
blaze@2.3.2
blaze-html-templates@1.1.2
blaze-tools@1.0.10
boilerplate-generator@1.2.0
caching-compiler@1.1.9
boilerplate-generator@1.3.1
caching-compiler@1.1.11
caching-html-compiler@1.1.2
callback-hook@1.0.10
check@1.2.5
@@ -27,31 +27,30 @@ chuangbo:marked@0.3.5_1
coffeescript@1.11.1_4
dburles:collection-helpers@1.1.0
dburles:mongo-collection-instances@0.3.5
ddp@1.3.1
ddp-client@2.1.3
ddp-common@1.2.9
ddp@1.4.0
ddp-client@2.2.0
ddp-common@1.3.0
ddp-rate-limiter@1.0.7
ddp-server@2.0.2
ddp-server@2.1.1
deps@1.0.12
diff-sequence@1.0.7
differential:vulcanize@3.0.0
dynamic-import@0.1.3
ecmascript@0.8.2
ecmascript-runtime@0.4.1
ecmascript-runtime-client@0.4.3
ecmascript-runtime-server@0.4.1
dynamic-import@0.2.1
ecmascript@0.9.0
ecmascript-runtime@0.5.0
ecmascript-runtime-client@0.5.0
ecmascript-runtime-server@0.5.0
ecwyne:mathjs@0.25.0
ejson@1.0.14
ejson@1.1.0
email@1.2.3
es5-shim@4.6.15
fastclick@1.0.13
geojson-utils@1.0.10
google-config-ui@1.0.0
google-oauth@1.2.4
google-oauth@1.2.5
hot-code-push@1.0.4
html-tools@1.0.11
htmljs@1.0.11
http@1.2.12
http@1.3.0
id-map@1.0.9
iron:controller@1.0.12
iron:core@1.0.11
@@ -64,41 +63,41 @@ iron:url@1.1.0
jquery@1.11.10
lai:collection-extensions@0.2.1_1
launch-screen@1.1.1
less@2.7.9
less@2.7.12
livedata@1.0.18
localstorage@1.1.1
logging@1.1.17
localstorage@1.2.0
logging@1.1.19
matb33:collection-hooks@0.8.4
mdg:validation-error@0.5.1
meteor@1.7.2
meteor-base@1.1.0
meteor@1.8.2
meteor-base@1.2.0
meteorhacks:subs-manager@1.6.4
minifier-css@1.2.16
minifier-js@2.1.3
minimongo@1.3.1
mobile-experience@1.0.4
minifier-js@2.2.2
minimongo@1.4.3
mobile-experience@1.0.5
mobile-status-bar@1.0.14
modules@0.10.0
modules-runtime@0.8.0
momentjs:moment@2.18.1
mongo@1.2.2
mongo-dev-server@1.0.1
modules@0.11.3
modules-runtime@0.9.2
momentjs:moment@2.20.1
mongo@1.3.1
mongo-dev-server@1.1.0
mongo-id@1.0.6
nikogosovd:multiple-uihooks@0.1.8
npm-bcrypt@0.9.3
npm-mongo@2.2.30
oauth@1.1.13
oauth2@1.1.11
npm-mongo@2.2.34
oauth@1.2.1
oauth2@1.2.0
observe-sequence@1.0.16
ongoworks:speakingurl@9.0.0
ordered-dict@1.0.9
percolate:migrations@0.9.8
percolate:synced-cron@1.3.2
promise@0.9.0
promise@0.10.1
raix:eventemitter@0.1.3
random@1.0.10
rate-limit@1.0.8
reactive-dict@1.1.9
reactive-dict@1.2.0
reactive-var@1.0.11
reload@1.1.11
retry@1.0.9
@@ -108,14 +107,14 @@ seba:minifiers-autoprefixer@1.0.1
service-configuration@1.0.11
session@1.1.7
sha@1.0.9
shell-server@0.2.4
shell-server@0.3.1
softwarerero:accounts-t9n@1.3.11
spacebars@1.0.15
spacebars-compiler@1.1.3
splendido:accounts-emails-field@1.2.0
splendido:accounts-meld@1.3.1
srp@1.0.10
standard-minifier-js@2.1.1
standard-minifier-js@2.2.3
templates:array@1.0.3
templating@1.3.2
templating-compiler@1.3.3
@@ -128,7 +127,7 @@ url@1.1.0
useraccounts:core@1.14.2
useraccounts:iron-routing@1.14.2
useraccounts:polymer@1.14.2
webapp@1.3.19
webapp@1.4.0
webapp-hashing@1.0.9
wizonesolutions:canonical@0.0.5
zimme:collection-behaviours@1.1.3

View File

@@ -164,9 +164,9 @@ Schemas.Character = new SimpleSchema({
//permissions
party: {type: String, regEx: SimpleSchema.RegEx.Id, optional: true},
owner: {type: String, regEx: SimpleSchema.RegEx.Id},
readers: {type: [String], regEx: SimpleSchema.RegEx.Id, defaultValue: []},
writers: {type: [String], regEx: SimpleSchema.RegEx.Id, defaultValue: []},
owner: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
readers: {type: [String], regEx: SimpleSchema.RegEx.Id, defaultValue: [], index: 1},
writers: {type: [String], regEx: SimpleSchema.RegEx.Id, defaultValue: [], index: 1},
color: {
type: String,
allowedValues: _.pluck(colorOptions, "key"),
@@ -185,11 +185,13 @@ Schemas.Character = new SimpleSchema({
type: String,
defaultValue: "whitelist",
allowedValues: ["whitelist", "public"],
index: 1,
},
"settings.swapStatAndModifier": {type: Boolean, defaultValue: false},
"settings.exportFeatures": {type: Boolean, defaultValue: true},
"settings.exportAttacks": {type: Boolean, defaultValue: true},
"settings.exportDescription": {type: Boolean, defaultValue: true},
"settings.newUserExperience": {type: Boolean, optional: true},
});
Characters.attachSchema(Schemas.Character);
@@ -297,6 +299,7 @@ Characters.calculate = {
var fieldSelector = {};
fieldSelector[fieldName] = 1;
var char = Characters.findOne(charId, {fields: fieldSelector});
if (!char) return;
var field = char[fieldName];
if (field === undefined){
throw new Meteor.Error(
@@ -330,6 +333,7 @@ Characters.calculate = {
},
attributeValue: memoize(function(charId, attributeName){
var attribute = Characters.calculate.getField(charId, attributeName);
if (!attribute) return;
//base value
var value = Characters.calculate.attributeBase(charId, attributeName);
//plus adjustment
@@ -341,6 +345,7 @@ Characters.calculate = {
}),
skillMod: memoize(preventLoop(function(charId, skillName){
var skill = Characters.calculate.getField(charId, skillName);
if (!skill) return;
//get the final value of the ability score
var ability = Characters.calculate.attributeValue(charId, skill.ability);
@@ -392,7 +397,6 @@ Characters.calculate = {
return prof && prof.value || 0;
}),
passiveSkill: memoize(function(charId, skillName){
var skill = Characters.calculate.getField(charId, skillName);
var mod = +Characters.calculate.skillMod(charId, skillName);
var value = 10 + mod;
Effects.find(
@@ -553,6 +557,10 @@ if (Meteor.isServer){
});
Characters.before.insert(function(userId, doc) {
doc.urlName = getSlug(doc.name, {maintainCase: true}) || "-";
// The first character a user creates should have the new user experience
if (!Characters.find({owner: userId}).count()){
doc.settings.newUserExperience = true;
}
});
}

View File

@@ -1,3 +1,75 @@
Schemas.UserProfile = new SimpleSchema({
username: {
type: String,
optional: true,
},
});
Schemas.User = new SimpleSchema({
username: {
type: String,
optional: true,
},
profile: {
type: Schemas.UserProfile,
optional: true,
},
emails: {
type: Array,
optional: true,
},
"emails.$": {
type: Object,
},
"emails.$.address": {
type: String,
regEx: SimpleSchema.RegEx.Email,
},
"emails.$.verified": {
type: Boolean,
},
registered_emails: {
type: Array,
optional: true,
},
"registered_emails.$": {
type: Object,
blackbox: true,
},
createdAt: {
type: Date
},
services: {
type: Object,
optional: true,
blackbox: true,
},
roles: {
type: Object,
optional: true,
blackbox: true,
},
roles: {
type: Array,
optional: true,
},
"roles.$": {
type: String
},
// In order to avoid an 'Exception in setInterval callback' from Meteor
heartbeat: {
type: Date,
optional: true,
},
apiKey: {
type: String,
index: 1,
optional: true,
},
});
Meteor.users.attachSchema(Schemas.User);
Meteor.users.allow({
update: function(userId, doc, fields, modifier) {
if (
@@ -21,3 +93,13 @@ Meteor.users.allow({
}
}
});
if (Meteor.isServer) Meteor.methods({
generateMyApiKey() {
var user = Meteor.users.findOne(this.userId);
if (!user) return;
if (user && user.apiKey) return;
var apiKey = Random.id(30);
Meteor.users.update(this.userId, {$set: {apiKey}});
},
});

View File

@@ -4,7 +4,11 @@ Router.map(function() {
where: "server",
action: function() {
this.response.setHeader("Content-Type", "application/json");
this.response.end(vMixCharacter(this.params._id));
var query = this.params.query;
var key = query && query.key;
ifKeyValid(key, this.response, () =>
this.response.end(vMixCharacter(this.params._id))
);
},
});
this.route("vmixParty", {
@@ -12,7 +16,38 @@ Router.map(function() {
where: "server",
action: function() {
this.response.setHeader("Content-Type", "application/json");
this.response.end(vMixParty(this.params._id));
var query = this.params.query;
var key = query && query.key;
ifKeyValid(key, this.response, () =>
this.response.end(vMixParty(this.params._id))
);
},
});
});
var ifKeyValid = function(apiKey, response, callback){
if (!apiKey){
response.writeHead(403, "You must use an api key to access this api");
response.end();
} else if (!isKeyValid(apiKey)){
response.writeHead(403, "API key is invalid");
response.end();
} else if (isRateLimited(apiKey)){
response.writeHead(429, "Too many requests");
response.end();
} else {
rateLimiter.increment({apiKey})
callback();
}
};
var isKeyValid = function(apiKey){
return !!Meteor.users.findOne({apiKey});
};
var rateLimiter = new RateLimiter();
rateLimiter.addRule({apiKey: String}, 2, 10000);
var isRateLimited = function(apiKey){
return !rateLimiter.check({apiKey}).allowed;
};

View File

@@ -78,6 +78,35 @@ Router.map(function() {
fastRender: true,
});
this.route("printedCharacterSheet", {
path: "/character/:_id/:urlName/print",
waitOn: function(){
return [
subsManager.subscribe("singleCharacter", this.params._id),
];
},
data: function() {
var data = Characters.findOne(
{_id: this.params._id},
{fields: {_id: 1, name: 1, color: 1, writers: 1, readers: 1}}
);
return data;
},
onAfterAction: function() {
var char = Characters.findOne({_id: this.params._id}, {fields: {name: 1}});
var name = char && char.name;
if (name){
document.title = name + " - Printing";
}
},
//analytics
trackPageView: false,
onRun: function() {
window.ga && window.ga("send", "pageview", "/print-character");
this.next();
},
});
this.route("library", {
path: "/library",
waitOn: function(){

View File

@@ -0,0 +1,174 @@
// jscs:disable
// https://github.com/chunksnbits/jquery-quickfit
(function ($) {
var Quickfit, QuickfitHelper, defaults, pluginName;
pluginName = 'quickfit';
defaults = {
min: 8,
max: 12,
tolerance: 0.02,
truncate: false,
width: null,
sampleNumberOfLetters: 10,
sampleFontSize: 12
};
QuickfitHelper = (function () {
var sharedInstance = null;
QuickfitHelper.instance = function (options) {
if (!sharedInstance) {
sharedInstance = new QuickfitHelper(options);
}
return sharedInstance;
};
function QuickfitHelper(options) {
this.options = options;
this.item = $('<span id="meassure"></span>');
this.item.css({
position: 'absolute',
left: '-1000px',
top: '-1000px',
'font-size': "" + this.options.sampleFontSize + "px"
});
$('body').append(this.item);
this.meassures = {};
}
QuickfitHelper.prototype.getMeassure = function (letter) {
var currentMeassure;
currentMeassure = this.meassures[letter];
if (!currentMeassure) {
currentMeassure = this.setMeassure(letter);
}
return currentMeassure;
};
QuickfitHelper.prototype.setMeassure = function (letter) {
var currentMeassure, index, sampleLetter, text, _ref;
text = '';
sampleLetter = letter === ' ' ? '&nbsp;' : letter;
for (index = 0, _ref = this.options.sampleNumberOfLetters - 1; 0 <= _ref ? index <= _ref : index >= _ref; 0 <= _ref ? index++ : index--) {
text += sampleLetter;
}
this.item.html(text);
currentMeassure = this.item.width() / this.options.sampleNumberOfLetters / this.options.sampleFontSize;
this.meassures[letter] = currentMeassure;
return currentMeassure;
};
return QuickfitHelper;
})();
Quickfit = (function () {
function Quickfit(element, options) {
this.$element = element;
this.options = $.extend({}, defaults, options);
this.$element = $(this.$element);
this._defaults = defaults;
this._name = pluginName;
this.quickfitHelper = QuickfitHelper.instance(this.options);
}
Quickfit.prototype.fit = function () {
var elementWidth;
if (!this.options.width) {
elementWidth = this.$element.width();
this.options.width = elementWidth - this.options.tolerance * elementWidth;
}
if (this.text = this.$element.attr('data-quickfit')) {
this.previouslyTruncated = true;
} else {
this.text = this.$element.text();
}
this.calculateFontSize();
if (this.options.truncate) this.truncate();
return {
$element: this.$element,
size: this.fontSize
};
};
Quickfit.prototype.calculateFontSize = function () {
var letter, textWidth, i;
textWidth = 0;
for (i = 0; i < this.text.length; ++i) {
letter = this.text.charAt(i);
textWidth += this.quickfitHelper.getMeassure(letter);
}
this.targetFontSize = parseInt(this.options.width / textWidth);
return this.fontSize = Math.max(this.options.min, Math.min(this.options.max, this.targetFontSize));
};
Quickfit.prototype.truncate = function () {
var index, lastLetter, letter, textToAdd, textWidth;
if (this.fontSize > this.targetFontSize) {
textToAdd = '';
textWidth = 3 * this.quickfitHelper.getMeassure('.') * this.fontSize;
index = 0;
while (textWidth < this.options.width && index < this.text.length) {
letter = this.text[index++];
if (lastLetter) textToAdd += lastLetter;
textWidth += this.fontSize * this.quickfitHelper.getMeassure(letter);
lastLetter = letter;
}
if (textToAdd.length + 1 === this.text.length) {
textToAdd = this.text;
} else {
textToAdd += '...';
}
this.textWasTruncated = true;
return this.$element.attr('data-quickfit', this.text).html(textToAdd);
} else {
if (this.previouslyTruncated) {
return this.$element.html(this.text);
}
}
};
return Quickfit;
})();
return $.fn.quickfit = function (options) {
var measurements = [];
// Separate measurements from repaints
// First calculate all measurements...
var $elements = this.each(function () {
var measurement = new Quickfit(this, options).fit();
measurements.push(measurement);
return measurement.$element;
});
// ... then apply the measurements.
for (var i = 0; i < measurements.length; i++) {
var measurement = measurements[i];
measurement.$element.css({ fontSize: measurement.size + 'px' });
}
return $elements;
};
})(jQuery, window);

View File

@@ -3,7 +3,8 @@ Template.registerHelper("canEditCharacter", function(charId) {
});
canEditCharacter = function(charId) {
var char = Characters.findOne(charId)
var char = Characters.findOne(charId);
if (!char) return false;
var userId = Meteor.userId();
return char.owner === userId ||
_.contains(char.writers, userId);

View File

@@ -0,0 +1,12 @@
Session.setDefault("isPrinting", false);
if (window.matchMedia) {
var mediaQueryList = window.matchMedia("print");
mediaQueryList.addListener(function(mql) {
if (mql.matches) {
Session.set("isPrinting", true);
Tracker.flush();
} else {
Session.set("isPrinting", false);
}
});
}

View File

@@ -0,0 +1,17 @@
removeDuplicateProficiencies = function(proficiencies) {
dict = {};
proficiencies.forEach(function(prof) {
if (prof.name in dict) { //if we have already gone over another proficiency for the same thing
if (dict[prof.name].value < prof.value) {
dict[prof.name] = prof; //then take the new one if it's higher, otherwise leave it
}
} else {
dict[prof.name] = prof; //if it wasn't already there, store it
}
});
profs = []
_.forEach(dict, function(prof) {
profs.push(prof);
})
return profs;
};

View File

@@ -0,0 +1,17 @@
@keyframes bounce {
from {
transform: translate(0px,0px);
}
to {
transform: translate(0px,-16px);
}
}
.bounce{
animation-name: bounce;
animation-duration: 0.3s;
animation-direction: alternate;
animation-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
animation-delay: 0s;
animation-iteration-count: infinite;
}

View File

@@ -10,7 +10,7 @@ Template.deleteCharacterConfirmation.helpers({
if (Template.instance().canDelete.get()) {
return "background: #d23f31; color: white;";
}
}
},
});
Template.deleteCharacterConfirmation.events({
@@ -20,9 +20,7 @@ Template.deleteCharacterConfirmation.events({
},
"click #deleteButton": function(event, instance) {
if (instance.find("#nameInput").value === this.name) {
popDialogStack();
Router.go("/characterList");
Characters.remove(this._id);
popDialogStack(true);
}
},
"click .cancelButton": function(event, instance){

View File

@@ -1,7 +1,7 @@
<template name="characterSheet">
<div class="fit layout vertical character-sheet">
<app-header fixed effects="waterfall">
<app-toolbar class="medium-tall {{colorClass}}">
<app-toolbar class="medium-tall {{colorClass}}" style="z-index: 2;">
<div top-item class="layout horizontal center">
<paper-icon-button icon="menu" drawer-toggle></paper-icon-button>
<div class="flex character-name">
@@ -20,6 +20,12 @@
<iron-icon icon="social:share" item-icon></iron-icon>
Share
</paper-icon-item>
<a href={{printUrl}}>
<paper-icon-item id="printButton">
<iron-icon icon="print" item-icon></iron-icon>
Print
</paper-icon-item>
</a>
<paper-icon-item id="characterSettings">
<iron-icon icon="settings" item-icon></iron-icon>
Settings
@@ -44,17 +50,18 @@
</div>
<div bottom-item>
<paper-tabs id="characterSheetTabs" selected={{selectedTab}} class="{{colorClass}}">
<paper-tab name="stats">Stats</paper-tab>
<paper-tab name="features">Features</paper-tab>
<paper-tab name="stats" class="{{#if shouldBounce 0}}bounce{{/if}}">Stats</paper-tab>
<paper-tab name="features" class="{{#if shouldBounce 1}}bounce{{/if}}">Features</paper-tab>
<paper-tab name="inventory">Inventory</paper-tab>
{{#unless hideSpellcasting}}
<paper-tab name="spells">Spells</paper-tab>
{{/unless}}
<paper-tab name="persona">Persona</paper-tab>
<paper-tab name="journal">Journal</paper-tab>
<paper-tab name="journal" class="{{#if shouldBounce 5}}bounce{{/if}}">Journal</paper-tab>
</paper-tabs>
</div>
</app-toolbar>
{{#if newUserExperience}}{{> newUserStepper}}{{/if}}
</app-header>
<div class="flex" style="position: relative;">
<iron-pages id="tabPages" class="fit" selected={{selectedTab}}>

View File

@@ -29,7 +29,7 @@ Template.characterSheet.onRendered(function() {
tabFabMenus = _.times(6, (n) =>
tabPages[n].find(".mini-holder")
);
})
});
//watch this character and make sure their encumbrance is updated
//trackEncumbranceConditions(this.data._id, this);
@@ -165,6 +165,12 @@ var getTab = function(charId){
};
Template.characterSheet.helpers({
printing: function(){
return Session.get("isPrinting");
},
printUrl: function(){
return `/character/${this._id}/${this.urlName || "-"}/print`
},
selectedTab: function(){
return getTab(this._id);
},
@@ -172,6 +178,18 @@ Template.characterSheet.helpers({
var char = Characters.findOne(this._id);
return char && char.settings.hideSpellcasting;
},
newUserExperience: function(){
var char = Characters.findOne(this._id);
return char && char.settings.newUserExperience;
},
shouldBounce: function(tab){
const selected = Session.get(this._id + ".selectedTab")
const step = Session.get("newUserExperienceStep");
if (selected == tab) return false;
return (tab === 1 && step === 0) ||
(tab === 5 && step === 1) ||
(tab === 0 && step === 2);
},
});
Template.characterSheet.events({
@@ -187,6 +205,12 @@ Template.characterSheet.events({
data: this,
template: "deleteCharacterConfirmation",
element: event.currentTarget.parentElement.parentElement,
callback: (result) => {
if (result === true){
Router.go("/characterList");
Tracker.afterFlush(() => Characters.remove(this._id));
}
},
});
},
"click #shareCharacter": function(event, instance){

View File

@@ -42,9 +42,20 @@
</template>
<template name="featureEdit">
{{#if showNewUserExperience}}
{{# infoBox}}
<p>
Features represent all the permanent things your character can do.
</p><p>
A feature can change a character's stats with effects,
or give the character proficiencies, attacks, and buffs.
</p><p>
Give the feature a name, and close it to continue.
</p>
{{/infoBox}}
{{/if}}
<!--name-->
<paper-input id="featureNameInput" class="fullwidth" label="Name" value={{name}}></paper-input>
<div class="layout horizontal center wrap justified">
<paper-dropdown-menu class=flex label="Enable Feature" style="flex-basis: 150px; max-width: 200px;">
<dicecloud-selector selected={{enabledSelection}} class="dropdown-content enabled-dropdown">

View File

@@ -47,6 +47,10 @@ Template.featureDetails.events({
});
Template.featureEdit.helpers({
showNewUserExperience: function(){
return Session.get("newUserExperienceStep") === 0 ||
Session.get("newUserExperienceStep") === 1;
},
usesSet: function(){
return _.isString(this.uses);
},

View File

@@ -74,7 +74,7 @@
checked={{enabled}}
disabled={{#unless canEditCharacter charId}}true{{/unless}}>
</paper-checkbox>
<paper-tooltip position="left">Feature enabled</paper-tooltip>
{{#simpleTooltip}}Feature enabled{{/simpleTooltip}}
</div>
{{/if}}
</div>
@@ -101,11 +101,13 @@
{{/each}}
</div>
{{#if canEditCharacter _id}}
<paper-fab id="addFeature"
class="floatyButton"
icon="add">
<paper-tooltip position="left">Add Feature</paper-tooltip>
</paper-fab>
<div class="floatyButton">
<paper-fab id="addFeature"
class="{{#if shouldFloatyButtonBounce}}bounce{{/if}}"
icon="add">
</paper-fab>
{{#simpleTooltip}}Add Feature{{/simpleTooltip}}
</div>
{{/if}}
</div>
</template>

View File

@@ -1,21 +1,3 @@
var removeDuplicateProficiencies = function(proficiencies) {
dict = {};
proficiencies.forEach(function(prof) {
if (prof.name in dict) { //if we have already gone over another proficiency for the same thing
if (dict[prof.name].value < prof.value) {
dict[prof.name] = prof; //then take the new one if it's higher, otherwise leave it
}
} else {
dict[prof.name] = prof; //if it wasn't already there, store it
}
});
profs = []
_.forEach(dict, function(prof) {
profs.push(prof);
})
return profs;
};
Template.features.helpers({
features: function(){
var features = Features.find({charId: this._id}, {sort: {color: 1, name: 1}});
@@ -59,6 +41,10 @@ Template.features.helpers({
hasCharacters: function(string){
return string && string.match(/\S/);
},
shouldFloatyButtonBounce: function(){
const step = Session.get("newUserExperienceStep");
return step === 0 && Features.find({charId: this._id}).count() <= 1;
},
});
Template.features.events({

View File

@@ -110,12 +110,12 @@
<div class="paper-font-caption" style="margin-right: 8px">
{{round totalWeight}} lbs
</div>
<div>
<div style="position: relative;">
<paper-checkbox class="carriedCheckbox"
disabled={{#unless canEditCharacter charId}}true{{/unless}}
checked={{isCarried}}>
</paper-checkbox>
<paper-tooltip position="left"> Container carried</paper-tooltip>
{{#simpleTooltip}} Container carried{{/simpleTooltip}}
</div>
</div>
<div class="bottom list">
@@ -136,21 +136,21 @@
class="addContainer"
mini>
</paper-fab>
<paper-tooltip position="left"> New container </paper-tooltip>
{{#simpleTooltip class="always"}} Container {{/simpleTooltip}}
</div>
<div>
<paper-fab icon="av:library-books"
class="libraryItem"
mini>
</paper-fab>
<paper-tooltip position="left"> Library item </paper-tooltip>
{{#simpleTooltip class="always"}} Item from library {{/simpleTooltip}}
</div>
<div>
<paper-fab icon="note-add"
class="addItem"
mini>
</paper-fab>
<paper-tooltip position="left"> New item </paper-tooltip>
{{#simpleTooltip class="always"}} Item {{/simpleTooltip}}
</div>
{{/fabMenu}}
{{/if}}

View File

@@ -53,7 +53,7 @@
</div>
<div class="bottom list">
<div class="item-slot">
<div class="item race layout horizontal center">
<div class="item race layout horizontal center {{#if shouldRaceBounce}}bounce{{/if}}">
{{race}}
</div>
</div>
@@ -83,9 +83,12 @@
</div>
<div class="fab-buffer"></div>
{{#if canEditCharacter _id}}
<paper-fab id="addNote"
class="floatyButton"
icon="add"
title="Add"></paper-fab>
<div class="floatyButton">
<paper-fab id="addNote"
icon="add"
title="Add">
</paper-fab>
{{#simpleTooltip}}Add Note{{/simpleTooltip}}
</div>
{{/if}}
</template>

View File

@@ -50,6 +50,9 @@ Template.journal.helpers({
var char = Characters.findOne(this._id, {fields: {race: 1}});
return char && char.race;
},
shouldRaceBounce: function(){
return Session.get("newUserExperienceStep") === 1;
},
});
Template.journal.events({

View File

@@ -1,11 +1,34 @@
<template name="raceDialog">
{{#baseDialog title="Race" class=color hideColor="true" hideDelete="true" startEditing=startEditing}}
{{#if showNewUserExperience}}
{{#infoBox}}
{{#if stepComplete}}
<p>You can add all the effects you need to represent how your race affects your character's attributes.</p>
{{else}}
<p>Click the edit button to edit your race and add a racial effect</p>
{{/if}}
{{/infoBox}}
{{/if}}
<div class="horizontal layout center-justified paper-font-display2">
{{race}}
</div>
{{> effectsViewList charId=charId parentId=charId parentGroup="racial"}}
{{> proficiencyViewList charId=charId parentId=charId parentGroup="racial"}}
{{else}}
{{#if showNewUserExperience}}
{{#infoBox}}
{{#if stepComplete}}
<p>You can add all the effects you need to represent how your race affects your character's attributes.</p>
{{else}}
<p>
Add an effect with the following options: <br>
Attribute: <b>stats > speed</b> <br>
Operation: <b>Base Value</b> <br>
Value: <b>30</b> (might be different for some races)
</p>
{{/if}}
{{/infoBox}}
{{/if}}
<paper-input id="raceInput" label="Race" value={{race}}></paper-input>
{{> effectsEditList parentId=charId parentCollection="Characters" charId=charId parentGroup="racial"}}
{{> proficiencyEditList parentId=charId parentCollection="Characters" charId=charId parentGroup="racial"}}

View File

@@ -19,4 +19,10 @@ Template.raceDialog.helpers({
var char = Characters.findOne(this.charId, {fields: {color: 1}});
if (char) return getColorClass(char.color);
},
stepComplete: function(){
return Session.get("newUserExperienceStep") > 1;
},
showNewUserExperience: function(){
return Session.get("newUserExperienceStep") >= 1;
},
});

View File

@@ -0,0 +1,12 @@
.newUserStepper {
height: 180px !important;
}
.newUserStepper paper-step .invalid-step-message {
color: #d13b2e;
visibility: hidden;
}
.newUserStepper paper-step[invalid] .invalid-step-message {
visibility: visible;
}

View File

@@ -0,0 +1,29 @@
<template name="newUserStepper">
<paper-stepper linear selected="0" class="newUserStepper">
<paper-step id="step0" label="Add a feature">
<p>
To get started, add a feature
</p>
</paper-step>
<paper-step id="step1" label="Add an effect">
<p>
Add a racial effect to set your speed
</p>
</paper-step>
<paper-step id="step2" label="See the effect in action">
<p>
View your speed stat
</p>
</paper-step>
<paper-step id="step3" label="Finish">
Done! If you get stuck, be sure to check out the <a href="/guide">guide</a>, or ask for help using the feedback form
<div class="layout vertical end">
<paper-button class="done-button" style="color: #d13b2e">Finish</paper-button>
</div>
</paper-step>
</paper-stepper>
</template>
<template name="newUserStepperPlaceholder">
<div style="height: 300px"></div>
</template>

View File

@@ -0,0 +1,58 @@
Template.newUserStepper.onRendered(function(){
Session.set("newUserExperienceStep", 0);
let stepper = this.find("paper-stepper");
_.defer(() => {
this.autorun((c) => {
var step = Session.get("newUserExperienceStep");
var hasFeatures = Features.find({charId: this.data._id}).count() > 1;
if (step === 0 && hasFeatures){
stepper.continue();
}
});
this.autorun((c) => {
var step = Session.get("newUserExperienceStep");
var hasEffect = !!Effects.find({
charId: this.data._id,
stat: "speed",
"parent.group": "racial",
operation: "base",
value: {$gt: 0},
}).count();
if (step === 1 && hasEffect){
stepper.continue();
}
});
this.autorun((c) => {
var step = Session.get("newUserExperienceStep");
if (step === 2 && Session.get("viewedSpeed")){
Session.set("viewedSpeed", undefined);
stepper.continue();
}
});
});
});
Template.newUserStepper.events({
"paper-stepper-progressed paper-stepper": function(event, template){
const step = template.find("paper-stepper").selected;
Session.set("newUserExperienceStep", step);
},
"paper-stepper-completed paper-stepper": function(event, template){
Session.set("newUserExperienceStep", undefined);
Session.set("showNewUserExperience", undefined);
Characters.update(this._id, {$unset: {"settings.newUserExperience": 1}});
},
"click .done-button": function(event, instance){
const stepper = instance.find("paper-stepper");
stepper.continue();
},
});
Template.stats.events({
"click .stat-card": function(event, instance){
var step = Session.get("newUserExperienceStep");
if (this.stat === "speed" && step === 2){
Session.set("viewedSpeed", true);
}
}
});

View File

@@ -0,0 +1,3 @@
.printedAbility .title.paper-font-subhead {
font-size: 2.5mm !important;
}

View File

@@ -0,0 +1,13 @@
<template name="printedAbility">
<div class="printedAbility layout vertical center double-border">
<div class="paper-font-subhead title flex layout horizontal center">
{{title}}
</div>
<div class="paper-font-display1 stat">
{{characterCalculate "attributeValue" ../_id ability}}
</div>
<div class="paper-font-subhead modifier">
{{abilityMod}}
</div>
</div>
</template>

View File

@@ -0,0 +1,9 @@
Template.printedAbility.helpers({
abilityMod: function() {
return signedString(
Characters.calculate.abilityMod(
Template.parentData()._id, this.ability
)
);
}
});

View File

@@ -0,0 +1,23 @@
<template name="printedAttack">
<div class="printedAttack" style="margin-bottom: 2mm">
<div class="layout horizontal">
<div class="paper-font-headline layout horizontal center"
style="margin-right: 1mm; min-width: 32px; text-align: right;">
{{evaluateAttackBonus charId attack}}
</div>
<div class="flex layout vertical">
<div class="paper-font-body2">
{{attack.name}}
</div>
<div>
{{evaluateDamage charId attack}}&nbsp;{{attack.damageType}}
</div>
{{#if attack.details}}
<div>
{{attack.details}}
</div>
{{/if}}
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,30 @@
Template.printedAttack.helpers({
evaluateAttackBonus: function(charId, attack) {
if (attack.parent.collection == "Spells") {
var spell = Spells.findOne(attack.parent.id);
if (spell) {
bonus = evaluate(charId, attack.attackBonus, {
"spellListId": spell.parent.id
});
}
} else {
var bonus = evaluate(charId, attack.attackBonus);
}
if (_.isFinite(bonus)) {
return bonus > 0 ? "+" + bonus : "" + bonus;
} else {
return bonus;
}
},
evaluateDamage: function(charId, attack) {
if (attack.parent.collection == "Spells") {
var spell = Spells.findOne(attack.parent.id);
if (spell) {
return evaluateSpellString(charId, spell.parent.id, attack.damage);
}
} else {
return evaluateString(charId, attack.damage);
}
},
});

View File

@@ -0,0 +1,190 @@
.printed .page {
width: 100%;
padding: 6mm;
page-break-inside: avoid;
page-break-after: always;
}
.printed .shrink-to-fit {
white-space: nowrap;
overflow: hidden;
}
.printed p {
margin-bottom: 1mm;
}
.printed .double-border {
position: relative;
padding: 11px 10px;
}
.printed .double-border > * {
position: relative;
}
.printed .double-border:before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
border: 16px solid transparent;
border-image-source: url(/png/doubleLineImageBorder.png);
border-image-slice: 110 126 fill;
border-image-repeat: stretch;
box-sizing: content-box;
}
.printed .double-border.printedAbility {
padding: 11px 6px 0;
margin-bottom: 3mm;
}
.printed .double-border.printedAbility:last-of-type {
margin-bottom: 0;
}
.printed .printedAbility .modifier {
position: relative;
top: 4px;
z-index: 1;
padding: 2px 18px;
background-image: url(/png/upwardPointingBorder.png);
background-position: center;
background-size: contain;
background-repeat: no-repeat;
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
.printed .octogon-border {
position: relative;
padding: 0 20px;
}
.printed .octogon-border:before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
border: 22px solid transparent;
border-image: url(/png/octogonBorder.png) 124 118 fill;
z-index: -1;
}
.printed iron-icon {
width: 16px;
min-width: 16px;
height: 16px;
min-height: 16px;
}
.printed .proficiencies, .printed .attacks, .printed .background {
overflow: hidden;
}
.printed .shield-background {
background: url(/png/shieldBorder.png);
background-size: cover;
background-repeat: no-repeat;
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
padding: 4px 8px 8px;
width: 80px;
height: 91px;
position: relative;
}
.printed .shield-background .paper-font-subhead {
width: 64px;
text-align: center;
line-height: 1.1;
}
.printed {
font-size: 3mm;
}
.printed .paper-font-body2 {
font-size: 3mm;
line-height: 4mm;
}
.printed .paper-font-subhead {
font-size: 3mm !important;
line-height: 3.5mm !important;
font-weight: bold !important;
text-transform: uppercase !important;
}
.printed .paper-font-subhead.modifier {
font-size: 4mm !important;
line-height: 6mm !important;
}
.printed .paper-font-display1 {
font-size: 7mm !important;
line-height: 12mm !important;
}
.printed .paper-font-headline {
font-size: 5mm !important;
line-height: 6mm !important;
}
.printed .lined-background {
background-image: url(/png/horizontalLine.png);
background-size: 100% 4mm;
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
@media screen {
.printed .page {
width: 210mm;
height: 297mm;
background: white;
margin: 8px;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14),
0 1px 5px 0 rgba(0, 0, 0, 0.12),
0 3px 1px -2px rgba(0, 0, 0, 0.2);
}
.printed .page-holder {
width: calc(210mm + 16px);
}
.printed {
overflow: auto;
padding-left:
}
}
@media print {
app-drawer {
display: none;
}
app-header {
display: none;
}
.printed {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 99;
background: #fff;
}
.printed .page-holder {
height: 100%
}
.printed .page {
height: 100%;
}
}

View File

@@ -0,0 +1,276 @@
<template name="printedCharacterSheet">
<div class="fit printed-character-sheet layout vertical">
<app-header fixed effects="waterfall">
<app-toolbar class="medium {{colorClass}} layout horizontal center" style="z-index: 2;">
<paper-icon-button icon="menu" drawer-toggle></paper-icon-button>
<paper-icon-button icon="arrow-back" class="backButton"></paper-icon-button>
<div class="flex character-name">
{{name}}
</div>
<div style="position: relative;">
<paper-icon-button icon="print" class="printButton"></paper-icon-button>
{{#simpleTooltip}} Print {{/simpleTooltip}}
</div>
</app-toolbar>
</app-header>
<div class="printed flex">
<div class="page-holder">
<div class="page">
<div class="layout vertical" style="height: 100%;">
<div class="layout horizontal center" style="margin-bottom: 4mm">
<img src="http://localhost:3000/crown-dice-logo-cropped-transparent.png" style="width: 60px; margin-right: 2mm">
<div class="characterName paper-font-title" style="margin-right: 4mm">
{{name}}
</div>
<div class="paper-font-body2">
<div>
{{#each classes}}
<span style="margin-right: 2mm;">
{{name}}&nbsp;{{level}}
</span>
{{/each}}
</div>
<div>
{{character.alignment}} {{character.gender}} {{character.race}}
</div>
</div>
<div class="flex layout vertical end" style="margin-right: 2mm;">
<div class="paper-font-body2 " style="font-size: 5mm !important;">
dicecloud.com
</div>
<div>
{{characterUrl}}
</div>
</div>
<canvas id="qrCode"></canvas>
</div>
<div class="columns layout horizontal flex">
<div class="col1 flex layout vertical">
<div class="layout vertical center-justified" style="min-height: 100px; margin-bottom: 4mm;">
<div class="initiative" style="margin-bottom: 2mm;">
{{> printedLongStat stat="" name="Inspiration" prefix=""}}
</div>
<div class="proficiencyBonus">
{{> printedLongStat stat="proficiencyBonus" name="Proficiency Bonus" prefix="+"}}
</div>
</div>
<div class="layout horizontal">
<div class="abilities layout vertical justified" style="margin-right: 4mm;">
{{> printedAbility ability="strength" title="Strength"}}
{{> printedAbility ability="dexterity" title="Dexterity"}}
{{> printedAbility ability="constitution" title="Constitution"}}
{{> printedAbility ability="intelligence" title="Intelligence"}}
{{> printedAbility ability="wisdom" title="Wisdom"}}
{{> printedAbility ability="charisma" title="Charisma"}}
</div>
<div class="flex layout vertical">
<div class="saves double-border" style="margin-bottom: 2mm">
<div>
{{> printedSkillRow name="Strength" skill="strengthSave"}}
{{> printedSkillRow name="Dexterity" skill="dexteritySave"}}
{{> printedSkillRow name="Constitution" skill="constitutionSave"}}
{{> printedSkillRow name="Intelligence" skill="intelligenceSave"}}
{{> printedSkillRow name="Wisdom" skill="wisdomSave"}}
{{> printedSkillRow name="Charisma" skill="charismaSave"}}
</div>
<div class="paper-font-subhead layout vertical center">
Saving Throws
</div>
</div>
<div class="skills double-border">
<div>
{{> printedSkillRow name="Acrobatics" skill="acrobatics"}}
{{> printedSkillRow name="Animal Handling" skill="animalHandling"}}
{{> printedSkillRow name="Arcana" skill="arcana"}}
{{> printedSkillRow name="Athletics" skill="athletics"}}
{{> printedSkillRow name="Deception" skill="deception"}}
{{> printedSkillRow name="History" skill="history"}}
{{> printedSkillRow name="Insight" skill="insight"}}
{{> printedSkillRow name="Intimidation" skill="intimidation"}}
{{> printedSkillRow name="Investigation" skill="investigation"}}
{{> printedSkillRow name="Medicine" skill="medicine"}}
{{> printedSkillRow name="Nature" skill="nature"}}
{{> printedSkillRow name="Perception" skill="perception" showPassive="true"}}
{{> printedSkillRow name="Performance" skill="performance"}}
{{> printedSkillRow name="Persuasion" skill="persuasion"}}
{{> printedSkillRow name="Religion" skill="religion"}}
{{> printedSkillRow name="Sleight of Hand" skill="sleightOfHand"}}
{{> printedSkillRow name="Stealth" skill="stealth"}}
{{> printedSkillRow name="Survival" skill="survival"}}
</div>
<div class="paper-font-subhead layout vertical center">
Skills
</div>
</div>
</div>
</div>
<div class="proficiencies flex double-border" style="margin-top: 4mm">
<div class="paper-font-subhead layout vertical center" style="margin-bottom: 2mm;">
Proficiencies
</div>
<div class="layout horizontal">
<div class="flex" style="margin-right: 2mm">
{{#if armorProfs.length}}
<div class="paper-font-subhead" style="margin-bottom: 1mm;">Armor</div>
{{/if}}
{{#each armorProfs}}
{{> printedProficiency}}
{{/each}}
{{#if weaponProfs.length}}
<div class="paper-font-subhead" style="margin: 2mm 0 1mm;">Weapons</div>
{{/if}}
{{#each weaponProfs}}
{{> printedProficiency}}
{{/each}}
</div>
{{#if toolProfs.length}}
<div class="flex">
<div class="paper-font-subhead" style="margin-bottom: 1mm;">Tools</div>
{{#each toolProfs}}
{{> printedProficiency}}
{{/each}}
</div>
{{/if}}
</div>
</div>
</div>
<div class="col2 flex layout vertical" style="margin-left: 4mm; margin-right: 4mm;">
<div class="layout horizontal center justified" style="min-height: 100px; margin-bottom: 4mm;">
<div class="armor">
{{> printedSquareStat stat="armor" name="Armor Class" class="shield-background"}}
</div>
<div class="inititive">
{{> printedSquareStat stat="initiative" name="Initiative" isSkill="true" class="double-border"}}
</div>
<div class="speed">
{{> printedSquareStat stat="speed" name="Speed" class="double-border"}}
</div>
</div>
<div class="hitpoints layout vertical double-border" style="margin-bottom: 2mm;">
<div>
Hit point maximum:
<span class="paper-font-subhead">
{{characterCalculate "attributeBase" _id "hitPoints"}}
</span>
</div>
<div class="flex" style="width: 3cm; height: 2cm;">
<!-- Space for writing -->
</div>
<div class="layout vertical center paper-font-subhead">
Hit Points
</div>
</div>
<div class="tempHitpoints layout vertical double-border" style="margin-bottom: 2mm;">
<div style="width: 3cm; height: 1.5cm;">
<!-- Space for writing -->
</div>
<div class="layout vertical center paper-font-subhead">
Temporary Hit Points
</div>
</div>
<div class="layout horizontal" style="margin-bottom: 4mm;">
<div class="hitDice double-border flex layout vertical" style="margin-right: 2mm;">
<div>
Total:
<span class="paper-font-subhead" style="text-transform: none !important;">
{{hitDiceTotal}}
</span>
</div>
<div class="flex" style="min-height: 1cm;">
<!-- Space for writing -->
</div>
<div class="paper-font-subhead layout vertical center">
Hit Dice
</div>
</div>
<div class="deathSaves layout vertical center double-border">
<div class="" style="margin-bottom: 1mm;">
Successes
</div>
<div class="layout horizontal center">
<iron-icon icon="radio-button-unchecked"></iron-icon>
<iron-icon icon="radio-button-unchecked"></iron-icon>
<iron-icon icon="radio-button-unchecked"></iron-icon>
</div>
<div class="" style="margin: 1mm 0;">
Failures
</div>
<div class="layout horizontal center">
<iron-icon icon="radio-button-unchecked"></iron-icon>
<iron-icon icon="radio-button-unchecked"></iron-icon>
<iron-icon icon="radio-button-unchecked"></iron-icon>
</div>
<div class="paper-font-subhead layout vertical center" style="margin-top: 2mm;">
Death Saves
</div>
</div>
</div>
<div class="attacks double-border flex">
<div class="paper-font-subhead layout vertical center" style="margin-bottom: 2mm;">
Attacks
</div>
{{#each attack in attacks}}
{{> printedAttack attack=attack charId=_id}}
{{/each}}
</div>
</div>
<div class="col3 flex layout vertical">
<div class="Languages double-border" style="min-height: 100px; margin-bottom: 4mm;">
<div class="paper-font-subhead layout vertical center" style="margin-bottom: 2mm;">
Languages
</div>
<div class="layout horizontal">
<div class="flex" style="margin-right: 2mm;">
{{#each languageProfs.left}}
{{> printedProficiency}}
{{/each}}
</div>
{{#if languageProfs.right.length}}
<div class="flex">
{{#each languageProfs.right}}
{{> printedProficiency}}
{{/each}}
</div>
{{/if}}
</div>
</div>
<div class="traits double-border">
{{#markdown}}{{evaluateShortString character._id character.personality}}{{/markdown}}
<div class="paper-font-subhead layout vertical center">
Personality traits
</div>
</div>
<div class="ideals double-border" style="margin-top: 2mm">
{{#markdown}}{{evaluateShortString character._id character.ideals}}{{/markdown}}
<div class="paper-font-subhead layout vertical center">
Ideals
</div>
</div>
<div class="bonds double-border" style="margin-top: 2mm">
{{#markdown}}{{evaluateShortString character._id character.bonds}}{{/markdown}}
<div class="paper-font-subhead layout vertical center">
Bonds
</div>
</div>
<div class="flaws double-border" style="margin-top: 2mm">
{{#markdown}}{{evaluateShortString character._id character.flaws}}{{/markdown}}
<div class="paper-font-subhead layout vertical center">
Flaws
</div>
</div>
<div class="background double-border flex layout vertical" style="margin-top: 2mm">
<div class="paper-font-subhead layout vertical center" style="margin-bottom: 4mm">
Notes
</div>
<div class="flex lined-background">
<!-- lined space for writing -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,80 @@
import QRCode from "qrcode"
Template.printedCharacterSheet.onRendered(function(){
// Quickfit is only called once on rendering, text will not resize reactively
this.$(".shrink-to-fit").quickfit({
min: 7,
max: 36,
truncate: true,
});
let url = `https://dicecloud.com/character/${this.data._id}`;
let canvas = this.find("#qrCode");
QRCode.toCanvas(canvas, url, {
margin: 0,
width: 200,
}, function(error){
$(canvas).css("width", "60px").css("height", "60px");
if (error) console.error(error)
});
});
Template.printedCharacterSheet.helpers({
character(){
return Characters.findOne(this._id);
},
classes: function(){
return Classes.find({charId: this._id}, {sort: {createdAt: 1}});
},
weaponProfs: function(){
var profs = Proficiencies.find({charId: this._id, type: "weapon"});
return removeDuplicateProficiencies(profs);
},
armorProfs: function(){
var profs = Proficiencies.find({charId: this._id, type: "armor"});
return removeDuplicateProficiencies(profs);
},
toolProfs: function(){
var profs = Proficiencies.find({charId: this._id, type: "tool"});
return removeDuplicateProficiencies(profs);
},
languageProfs: function(){
var profs = Proficiencies.find({charId: this._id, type: "language"});
profs = removeDuplicateProficiencies(profs);
if (profs.length > 3){
var halfway = Math.floor(profs.length / 2);
var left = profs.slice(0, halfway);
var right = profs.slice(halfway);
return {left, right};
} else {
return {left: profs, right: []};
}
},
attacks: function(){
return Attacks.find(
{charId: this._id, enabled: true},
{sort: {color: 1, name: 1}});
},
hitDiceTotal: function(){
let d6 = Characters.calculate.attributeValue(this._id, "d6HitDice");
let d8 = Characters.calculate.attributeValue(this._id, "d8HitDice");
let d10 = Characters.calculate.attributeValue(this._id, "d10HitDice");
let d12 = Characters.calculate.attributeValue(this._id, "d12HitDice");
d6 = d6 ? d6 + "d6" : "";
d8 = d8 ? d8 + "d8" : "";
d10 = d10 ? d10 + "d10" : "";
d12 = d12 ? d12 + "d12" : "";
return [d6, d8, d10, d12].filter(Boolean).join(" ");
},
characterUrl: function(){
return `/character/${this._id}`
},
});
Template.printedCharacterSheet.events({
"click .printButton": function(event, instance){
print();
},
"click .backButton": function(event, instance){
history && history.back();
},
});

View File

@@ -0,0 +1,20 @@
.printedLongStat .title {
white-space: nowrap;
margin-left: 2mm;
}
.printedLongStat .numbers {
z-index: 1;
min-width: 74px;
min-height: 45px;
}
.printed .printedLongStat.double-border{
padding: 0;
}
.printed .printedLongStat.double-border:before {
top: 4px;
bottom: 4px;
left: 33px;
}

View File

@@ -0,0 +1,16 @@
<template name="printedLongStat">
<div class="printedLongStat layout horizontal double-border">
<div class="numbers paper-font-display1 octogon-border">
{{#if stat}}
{{#if isSkill}}
{{prefix}}{{skillMod}}
{{else}}
{{prefix}}{{characterCalculate "attributeValue" ../_id stat}}
{{/if}}
{{/if}}
</div>
<div class="paper-font-subhead title flex layout horizontal center">
{{name}}
</div>
</div>
</template>

View File

@@ -0,0 +1,9 @@
Template.printedLongStat.helpers({
skillMod: function() {
return signedString(
Characters.calculate.skillMod(
Template.parentData()._id, this.stat
)
);
},
});

View File

@@ -0,0 +1,3 @@
.printedProficiency iron-icon {
margin-right: 2mm;
}

View File

@@ -0,0 +1,6 @@
<template name="printedProficiency">
<div class="printedProficiency layout horizontal center">
<iron-icon icon="{{profIcon}}"></iron-icon>
<div>{{getName}}</div>
</div>
</template>

View File

@@ -0,0 +1,40 @@
Template.printedProficiency.helpers({
profIcon: function(){
var prof = this.value;
if (prof > 0 && prof < 1) return "image:brightness-2";
if (prof === 1) return "image:brightness-1";
if (prof > 1) return "av:album";
return "radio-button-off";
},
getName: function(){
if (this.type === "skill") return skills[this.name];
if (this.type === "save") return saves[this.name];
return this.name;
},
});
Template.printedProficiency.events({
"click .proficiency": function(event, instance){
if (this.parent.collection == "Characters") {
if (this.parent.group == "background") {
pushDialogStack({
template: "backgroundDialog",
data: {
"charId": this.charId,
"field":"background",
"title":"Background",
"color":"j",
},
element: event.currentTarget,
})
return;
}
}
openParentDialog({
parent: this.parent,
charId: this.charId,
element: event.currentTarget,
});
}
});

View File

@@ -0,0 +1,10 @@
.printedSkillRow {
height: 24px;
min-width: 140px;
}
.printedSkillRow .skill-mod {
width: 36px;
text-align: center;
font-size: 3.5mm;
}

View File

@@ -0,0 +1,21 @@
<template name="printedSkillRow">
<div class="printedSkillRow layout horizontal center">
<iron-icon icon="{{profIcon}}"></iron-icon>
{{#if failSkill}}
<div class="fail skill-mod">fail</div>
{{else}}
<div class="{{advantage}} skill-mod">
{{skillMod}}
</div>
{{/if}}
<div flex>
{{name}}
{{#if conditionalCount}}
*
{{/if}}
{{#if showPassive}}
({{characterCalculate "passiveSkill" ../_id skill}})
{{/if}}
</div>
</div>
</template>

View File

@@ -0,0 +1,41 @@
Template.printedSkillRow.helpers({
skillMod: function() {
return signedString(
Characters.calculate.skillMod(
Template.parentData()._id, this.skill
)
);
},
profIcon: function(){
var charId = Template.parentData()._id;
var prof = Characters.calculate.proficiency(charId, this.skill);
if (prof > 0 && prof < 1) return "image:brightness-2";
if (prof === 1) return "image:brightness-1";
if (prof > 1) return "av:album";
return "radio-button-unchecked";
},
failSkill: function(){
var charId = Template.parentData()._id;
return Effects.find({
charId: charId,
stat: this.skill,
enabled: true,
operation: "fail",
}).count();
},
advantage: function(){
var charId = Template.parentData()._id;
var advantage = Characters.calculate.advantage(charId, this.skill);
if (advantage > 0) return "advantage";
if (advantage < 0) return "disadvantage";
},
conditionalCount: function(){
var charId = Template.parentData()._id;
return Effects.find({
charId: charId,
stat: this.skill,
enabled: true,
operation: "conditional",
}).count();
},
});

View File

@@ -0,0 +1,7 @@
.printedSquareStat {
min-width: 67px;
}
.printedSquareStat .title.paper-font-subhead {
font-size: 2.5mm !important;
}

View File

@@ -0,0 +1,14 @@
<template name="printedSquareStat">
<div class="printedSquareStat layout vertical center {{class}}">
<div class="numbers paper-font-display1">
{{#if isSkill}}
{{prefix}}{{skillMod}}
{{else}}
{{prefix}}{{characterCalculate "attributeValue" ../_id stat}}
{{/if}}
</div>
<div class="paper-font-subhead title">
{{name}}
</div>
</div>
</template>

View File

@@ -0,0 +1,9 @@
Template.printedSquareStat.helpers({
skillMod: function() {
return signedString(
Characters.calculate.skillMod(
Template.parentData()._id, this.stat
)
);
},
});

View File

@@ -53,22 +53,22 @@
{{numPrepared}} / {{evaluate charId maxPrepared}}
</div>
{{/if}}
<div>
<paper-tooltip position="left">
Done
</paper-tooltip>
<div style="position: relative;">
<paper-icon-button class="finishPrep" icon="done">
</paper-icon-button>
{{#simpleTooltip}}
Done
{{/simpleTooltip}}
</div>
{{else}}
<div>
<paper-tooltip position="left">
Change prepared spells
</paper-tooltip>
<div style="position: relative;">
<paper-icon-button class="prepSpells"
disabled={{#unless canEditCharacter charId}}true{{/unless}}
icon="book">
</paper-icon-button>
{{#simpleTooltip}}
Change prepared spells
{{/simpleTooltip}}
</div>
{{/if}}
</div>
@@ -124,32 +124,31 @@
{{#if canEditCharacter _id}}
{{#fabMenu}}
<div>
<paper-tooltip position="left">
New spell list
</paper-tooltip>
<paper-fab icon="work"
class="addSpellList"
mini>
</paper-fab>
{{#simpleTooltip class="always"}}
Spell list
{{/simpleTooltip}}
</div>
<div>
<paper-tooltip position="left">
Spell library
</paper-tooltip>
<paper-fab icon="av:library-books"
class="librarySpell"
mini>
</paper-fab>
{{#simpleTooltip class="always"}}
Spell from library
{{/simpleTooltip}}
</div>
<div>
<paper-tooltip position="left">
New spell
</paper-tooltip>
<paper-fab icon="note-add"
class="addSpell"
mini>
</paper-fab>
{{#simpleTooltip class="always"}}
Spell
{{/simpleTooltip}}
</div>
{{/fabMenu}}
{{/if}}

View File

@@ -279,23 +279,23 @@ Template.spells.events({
collection: "SpellLists",
};
spell.prepared = "prepared";
Spells.insert(spell);
let insertedSpellId = Spells.insert(spell);
// Copy over attacks and effects
_.each(rawSpell.attacks, (attack) => {
if (!("attackBonus" in attack)) {attack.attackBonus = "attackBonus"} //if no attack bonus provided, use spell list's
attack.charId = charId;
attack.parent = {id: spellId, collection: "Spells"};
attack.parent = {id: insertedSpellId, collection: "Spells"};
Attacks.insert(attack);
});
_.each(rawSpell.effects, (effect) => {
effect.charId = charId;
effect.parent = {id: spellId, collection: "Spells"};
effect.parent = {id: insertedSpellId, collection: "Spells"};
Effects.insert(effect);
});
_.each(rawSpell.buffs, (buff) => {
buff.charId = charId;
buff.parent = {id: spellId, collection: "Spells"};
buff.parent = {id: insertedSpellId, collection: "Spells"};
buffId = Buffs.insert(buff);
_.each(buff.attacks, (attack) => {

View File

@@ -6,6 +6,16 @@
</template>
<template name="attributeDialogView">
{{#if showNewUserExperience}}
{{#infoBox}}
<p>
This dialog shows how your speed is set by the effect you added to your character's race.
</p>
<p>
In DiceCloud you don't change stats directly, rather you add effects which impact your stats in different ways. This way, you can always tell where your stats came from, and how they got to their current value.
</p>
{{/infoBox}}
{{/if}}
<div class="layout horizontal center-justified end">
<div class="paper-font-display2">
{{attributeValue}}

View File

@@ -157,4 +157,9 @@ Template.attributeDialogView.helpers({
statValue: function(){
return evaluateEffect(this.charId, this);
},
showNewUserExperience: function(){
if (this.statName === "speed"){
return Session.get("newUserExperienceStep") >= 2;
}
},
});

View File

@@ -1,6 +1,6 @@
<template name="statCard">
<div>
<paper-material class="stat-card layout horizontal">
<paper-material class="stat-card layout horizontal {{#if bounce}}bounce{{/if}}">
<div class="numbers paper-font-display1">
{{#if isSkill}}
{{prefix}}{{skillMod}}

View File

@@ -15,7 +15,7 @@
<!--Armor-->
{{> statCard stat="armor" name="Armor Class" color="teal"}}
<!--Speed-->
{{> statCard stat="speed" name="Speed" color="teal"}}
{{> statCard stat="speed" name="Speed" color="teal" bounce=shouldSpeedBounce}}
<!--Initiative-->
{{> statCard stat="initiative" name="Initiative" color="indigo" isSkill="true"}}
<!--Proficiency Bonus-->

View File

@@ -8,6 +8,10 @@ Template.stats.helpers({
};
return Buffs.find(selector);
},
// New user experience
shouldSpeedBounce: function(){
return Session.get("newUserExperienceStep") === 2;
},
})
Template.stats.events({
@@ -84,8 +88,7 @@ Template.stats.events({
callback: (result) => {
if (!result) {
return;
}
else Meteor.call("giveCondition", this._id, result)
} else Meteor.call("giveCondition", this._id, result)
},
//returnElement: () => $(`[data-id='${itemId}']`).get(0),
})

View File

@@ -49,14 +49,14 @@
class="addParty"
mini>
</paper-fab>
<paper-tooltip position="left"> New Party </paper-tooltip>
{{#simpleTooltip class="always"}} New Party {{/simpleTooltip}}
</div>
<div>
<paper-fab icon="face"
class="addCharacter"
mini>
</paper-fab>
<paper-tooltip position="left"> New Character </paper-tooltip>
{{#simpleTooltip class="always"}} New Character {{/simpleTooltip}}
</div>
{{/fabMenu}}
</div>

View File

@@ -85,7 +85,7 @@
<div>
On the official subreddit
</div>
<a href="http://www.reddit.com/r/dicecloud/">
<a href="http://www.reddit.com/r/dicecloud/" target="_blank">
<paper-button class="redditButton">
/r/dicecloud
</paper-button>
@@ -93,14 +93,14 @@
</div>
<div class="layout vertical center">
<div class="paper-font-headline">
Get involved
Open Source
</div>
<div>
Shape upcoming features and track bugs on the DiceCloud Trello board
Shape upcoming features, track bugs, and contribute to the DiceCloud codebase
</div>
<a href="https://trello.com/b/94M0SCnq/dicecloud-roadmap">
<paper-button class="trelloButton">
Trello Roadmap
<a href="https://github.com/ThaumRystra/DiceCloud1/" target="_blank">
<paper-button class="githubButton">
GitHub Repo
</paper-button>
</a>
</div>

View File

@@ -23,4 +23,17 @@
<meta name="msapplication-TileColor" content="#b91d1d">
<meta name="msapplication-TileImage" content="/mstile-144x144.png?v=lk6WXp6Pmj">
<meta name="theme-color" content="#d12929">
<style type="text/css" media="print">
@page {
margin: 0mm;
}
html {
margin: 0px;
}
* {
-webkit-transition: none !important;
transition: none !important;
}
</style>
</head>

View File

@@ -68,7 +68,7 @@
transform-origin: top left;
transition: top 400ms ease, left 400ms ease;
z-index: 3;
overflow: hidden;
overflow: visible;
}
.dialog-stack .dialog .testButton {

View File

@@ -0,0 +1,15 @@
.infoBox iron-icon {
color: #747474;
color: rgba(0,0,0,0.54);
height: 32px;
width: 32px;
margin-right: 12px;
}
.infoBox > div > p {
margin: 0;
}
.infoBox > div > p + p {
margin-top: 10px;
}

View File

@@ -0,0 +1,10 @@
<template name="infoBox">
<div class="layout horizontal center infoBox">
<div>
<iron-icon icon="info-outline"></iron-icon>
</div>
<div class="flex">
{{> Template.contentBlock}}
</div>
</div>
</template>

View File

@@ -25,7 +25,6 @@
{{/ simpleTooltip}}
</div>
<div class="brackets" style="position: relative">
<!--<paper-tooltip position="left" animation-delay="0">This field accepts formulae in {curly brackets}</paper-tooltip>-->
<iron-icon icon="dicecloud:code-braces"></iron-icon>
{{# simpleTooltip}}
This field accepts formulae in {curly brackets}

View File

@@ -1,4 +1,18 @@
.simple-tooltip:hover .tooltip {
.simple-tooltip {
pointer-events: none;
}
.simple-tooltip:active {
pointer-events: none;
}
/* Show the tooltip if a older sibling is hovered */
*:hover ~ .simple-tooltip > .tooltip {
opacity: 0.9;
}
/* Show the tooltip if parent is hovered */
*:hover > .simple-tooltip > .tooltip {
opacity: 0.9;
}
@@ -16,3 +30,7 @@
pointer-events: none;
white-space: nowrap;
}
.tooltip.always {
opacity: 0.9;
}

View File

@@ -1,6 +1,6 @@
<template name="simpleTooltip">
<div class="simple-tooltip fit">
<div class="tooltip">
<div class="simple-tooltip fit layout vertical center-justified">
<div class="tooltip {{class}}">
{{> Template.contentBlock}}
</div>
</div>

View File

@@ -1,4 +1,3 @@
.profile #at-nav-button {
color: #212121;
color: rgba(0,0,0,0.87);
.profile paper-button, .profile a, .profile #at-nav-button {
color: #d13b2e;
}

View File

@@ -27,7 +27,7 @@
{{#if verified}}
<span>
<iron-icon icon="check"></iron-icon>
<paper-tooltip>Verified</paper-tooltip>
{{#simpleTooltip}}Verified{{/simpleTooltip}}
</span>
{{/if}}
</div>
@@ -35,9 +35,36 @@
</td>
<td></td>
</tr>
<tr>
<td colspan="2">
<a href="/change-password">
<paper-button>Change password</paper-button>
</a>
</td>
</tr>
<tr>
<td>
API Key
</td>
<td class="apiKey">
{{#if apiKey}}
{{#unless showApiKey}}
<paper-button class="showApiKey">
Show
</paper-button>
{{else}}
{{apiKey}}
{{/unless}}
{{else}}
<paper-button class="generateMyApiKey">
Generate
</paper-button>
{{/if}}
</td>
</tr>
</table>
<div style="max-width: 250px">
{{> atForm}}
{{> atForm state="signIn"}}
</div>
{{> atNavButton }}
</paper-material>

View File

@@ -1,10 +1,17 @@
Template.profile.onCreated(function(){
this.showApiKey = new ReactiveVar(false);
});
Template.profile.helpers({
profileName: function() {
var user = Meteor.user();
return user.profile && user.profile.username ||
user.username ||
"Tap to set username";
}
},
showApiKey: function(){
return Template.instance().showApiKey.get();
},
});
Template.profile.events({
@@ -25,4 +32,11 @@ Template.profile.events({
data: {},
});
},
"click .showApiKey": function(event, instance){
instance.showApiKey.set(!instance.showApiKey.get());
},
"click .generateMyApiKey": function(event, instance){
Meteor.call("generateMyApiKey");
instance.showApiKey.set(true);
},
});

View File

@@ -3,7 +3,7 @@
<app-header-layout has-scrolling-region class="feedback flex">
<app-header fixed effects="waterfall">
<app-toolbar>
<div main-title>Feedback</div>
<div main-title>Change Username</div>
</app-toolbar>
</app-header>
<div class="form flex">

View File

@@ -49,6 +49,7 @@
"/custom_components/dicecloud-wrapper/dicecloud-wrapper.html",
"/custom_components/paper-checkbox/paper-checkbox.html",
"/custom_components/paper-diff-slider/paper-diff-slider.html",
"/custom_components/paper-stepper/paper-stepper.html",
"/custom_components/app-theme.html"
]
}

View File

@@ -6,3 +6,14 @@ abilities = [
"wisdom",
"charisma",
];
ABILITIES = abilities;
ABILITY_MODS = [
"strengthMod",
"dexterityMod",
"constitutionMod",
"intelligenceMod",
"wisdomMod",
"charismaMod",
];

View File

@@ -0,0 +1,34 @@
ATTRIBUTES = [
"strength",
"dexterity",
"constitution",
"intelligence",
"wisdom",
"charisma",
"hitPoints",
"tempHP",
"experience",
"proficiencyBonus",
"speed",
"armor",
"carryMultiplier",
"level1SpellSlots",
"level2SpellSlots",
"level3SpellSlots",
"level4SpellSlots",
"level5SpellSlots",
"level6SpellSlots",
"level7SpellSlots",
"level8SpellSlots",
"level9SpellSlots",
"ki",
"sorceryPoints",
"rages",
"superiorityDice",
"expertiseDice",
"rageDamage",
"d6HitDice",
"d8HitDice",
"d10HitDice",
"d12HitDice",
];

View File

@@ -19,3 +19,32 @@ SKILLS = [
"survival",
"initiative",
];
ALL_SKILLS = [
"strengthSave",
"dexteritySave",
"constitutionSave",
"intelligenceSave",
"wisdomSave",
"charismaSave",
"acrobatics",
"animalHandling",
"arcana",
"athletics",
"deception",
"history",
"insight",
"intimidation",
"investigation",
"medicine",
"nature",
"perception",
"performance",
"persuasion",
"religion",
"sleightOfHand",
"stealth",
"survival",
"initiative",
"dexterityArmor",
];

View File

@@ -1 +1,6 @@
subsManager = new SubsManager();
subsManager = new SubsManager({
// maximum number of cache subscriptions
cacheLimit: 5,
// any subscription will be expire after 1 minute, if it's not subscribed again
expireIn: 1,
});

View File

@@ -2,7 +2,7 @@ AccountsTemplates.configure({
//behaviour
confirmPassword: true,
enablePasswordChange: true,
enforceEmailVerification: true,
enforceEmailVerification: false,
overrideLoginErrors: false,
sendVerificationEmail: true,
lowercaseUsername: true,
@@ -21,35 +21,35 @@ AccountsTemplates.configure({
AccountsTemplates.configureRoute("changePwd", {
template: "titledAtForm",
layoutTemplate: 'layout',
layoutTemplate: "layout",
});
AccountsTemplates.configureRoute("enrollAccount", {
template: "titledAtForm",
layoutTemplate: 'layout',
layoutTemplate: "layout",
});
AccountsTemplates.configureRoute("forgotPwd", {
template: "titledAtForm",
layoutTemplate: 'layout',
layoutTemplate: "layout",
});
AccountsTemplates.configureRoute("resetPwd", {
template: "titledAtForm",
layoutTemplate: 'layout',
layoutTemplate: "layout",
});
AccountsTemplates.configureRoute("signIn", {
template: "titledAtForm",
layoutTemplate: 'layout',
layoutTemplate: "layout",
});
AccountsTemplates.configureRoute("signUp", {
template: "titledAtForm",
layoutTemplate: 'layout',
layoutTemplate: "layout",
});
AccountsTemplates.configureRoute("verifyEmail", {
template: "titledAtForm",
layoutTemplate: 'layout',
layoutTemplate: "layout",
});
AccountsTemplates.configureRoute("resendVerificationEmail", {
template: "titledAtForm",
layoutTemplate: 'layout',
layoutTemplate: "layout",
});
if (Meteor.isServer){

View File

@@ -1,5 +1,11 @@
characterExport = function(charId){
var char = Characters.findOne(charId);
var {
character, classes, effects, proficiencies,
} = getCharacterForComputation(charId);
var char = character;
computedCharacter = computeCharacter({
character, classes, effects, proficiencies,
});
if (!char) {
return {
error: charId + " character not found"
@@ -11,25 +17,87 @@ characterExport = function(charId){
};
}
var baseValue = function(attributeName){
return Characters.calculate.attributeBase(charId, attributeName);
var attribute = computedCharacter[attributeName];
return attribute && attribute.value;
};
var attributeValue = function(attributeName){
return Characters.calculate.attributeValue(charId, attributeName);
var base = baseValue(attributeName);
var adjustment = char[attributeName] && char[attributeName].adjustment;
return base + adjustment;
};
var abilityMod = function(attributeName){
return signedString(
Characters.calculate.abilityMod(charId, attributeName)
);
return signedString(getMod(attributeValue(attributeName)));
};
var skillMod = function(skillName){
return signedString(
Characters.calculate.skillMod(charId, skillName)
);
return signedString(baseValue(skillName));
};
var proficiency = function(skillName){
return Characters.calculate.proficiency(charId, skillName);
var skill = computedCharacter[skillName];
return skill && skill.proficiency;
};
var passiveSkill = function(skillName){
var attribute = computedCharacter[skillName];
if (!attribute) return;
return 10 + baseValue(skillName) + attribute.passiveAdd;
};
var experience = function(){
var xp = 0;
Experiences.find(
{charId: charId},
{fields: {value: 1}}
).forEach(function(e){
xp += e.value;
});
return xp;
};
var getClasses = function(){
return _.map(classes, c => `${c.name} ${c.level}`).join(", ");
};
var getHitDiceString = function(){
var d6 = baseValue("d6HitDice");
var d8 = baseValue("d8HitDice");
var d10 = baseValue("d10HitDice");
var d12 = baseValue("d12HitDice");
var con = abilityMod("constitution");
var string = "" +
(d6 ? `${d6}d6 + ` : "") +
(d8 ? `${d8}d8 + ` : "") +
(d10 ? `${d10}d10 + ` : "") +
(d12 ? `${d12}d12 + ` : "") +
con;
return string;
}
var damageMods = getDamageMods(charId);
var getSkills = function(charId){
var allSkills = [
{name: "acrobatics", attribute: "dexterity"},
{name: "animalHandling", attribute: "wisdom"},
{name: "arcana", attribute: "intelligence"},
{name: "athletics", attribute: "strength"},
{name: "deception", attribute: "charisma"},
{name: "history", attribute: "intelligence"},
{name: "insight", attribute: "wisdom"},
{name: "intimidation", attribute: "charisma"},
{name: "investigation", attribute: "intelligence"},
{name: "medicine", attribute: "wisdom"},
{name: "nature", attribute: "intelligence"},
{name: "perception", attribute: "wisdom"},
{name: "performance", attribute: "charisma"},
{name: "persuasion", attribute: "charisma"},
{name: "religion", attribute: "intelligence"},
{name: "sleightOfHand", attribute: "dexterity"},
{name: "stealth", attribute: "dexterity"},
{name: "survival", attribute: "wisdom"},
];
var skills = {};
_.each(allSkills, skill => {
var value = skillMod(skill.name);
var prof = proficiency(skill.name);
var name = skill.name.charAt(0).toUpperCase() + skill.name.slice(1);
skills[name] = value;
skills[name + "Proficiency"] = prof;
});
return skills;
};
var character = {
"Id": char._id,
"Name": char.name,
@@ -37,8 +105,8 @@ characterExport = function(charId){
"Alignment": char.alignment || "",
"Gender": char.gender || "",
"Race": char.race || "",
"Level": Characters.calculate.level(charId),
"Experience": Characters.calculate.experience(charId),
"Level": _.reduce(classes, (memo, cls) => memo + cls.level, 0),
"Experience": experience(),
"Class": getClasses(charId),
"HPBase": baseValue("hitPoints"),
"HPValue": attributeValue("hitPoints"),
@@ -47,7 +115,7 @@ characterExport = function(charId){
"Initiative": skillMod("initiative"),
"Speed": attributeValue("speed"),
"ProficiencyBonus": attributeValue("proficiencyBonus"),
"passivePerception": Characters.calculate.passiveSkill(charId, "perception"),
"passivePerception": passiveSkill("perception"),
"Languages": getLanguages(charId),
"Description": char.description || "",
@@ -72,9 +140,9 @@ characterExport = function(charId){
"WisdomMod": abilityMod("wisdom"),
"CharismaMod": abilityMod("charisma"),
"DamageVulnerabilities": damageMods.vulnerabilities,
"DamageResistances": damageMods.resistances,
"DamageImmunities": damageMods.immunities,
//"DamageVulnerabilities": damageMods.vulnerabilities,
//"DamageResistances": damageMods.resistances,
//"DamageImmunities": damageMods.immunities,
"StrengthSave": skillMod("strengthSave"),
"StrengthSaveProficiency": proficiency("strengthSave"),
@@ -112,21 +180,6 @@ characterExport = function(charId){
return character;
}
var getHitDiceString = function(charId){
var d6 = Characters.calculate.attributeBase(charId, "d6HitDice");
var d8 = Characters.calculate.attributeBase(charId, "d8HitDice");
var d10 = Characters.calculate.attributeBase(charId, "d10HitDice");
var d12 = Characters.calculate.attributeBase(charId, "d12HitDice");
var con = Characters.calculate.abilityMod(charId,"constitution");
var string = "" +
(d6 ? `${d6}d6 + ` : "") +
(d8 ? `${d8}d8 + ` : "") +
(d10 ? `${d10}d10 + ` : "") +
(d12 ? `${d12}d12 + ` : "") +
con;
return string;
}
var getArmorString = function(charId){
var bases = Effects.find({
charId: charId,
@@ -148,7 +201,7 @@ var getArmorString = function(charId){
strings = strings.concat(effects);
return strings.join(", ");
}
/*
var getDamageMods = function(charId){
// jscs:disable maximumLineLength
var multipliers = [
@@ -175,40 +228,7 @@ var getDamageMods = function(charId){
"vulnerabilities": _.map(multipliers["2"], names).join(", "),
};
}
var getSkills = function(charId){
var allSkills = [
{name: "acrobatics", attribute: "dexterity"},
{name: "animalHandling", attribute: "wisdom"},
{name: "arcana", attribute: "intelligence"},
{name: "athletics", attribute: "strength"},
{name: "deception", attribute: "charisma"},
{name: "history", attribute: "intelligence"},
{name: "insight", attribute: "wisdom"},
{name: "intimidation", attribute: "charisma"},
{name: "investigation", attribute: "intelligence"},
{name: "medicine", attribute: "wisdom"},
{name: "nature", attribute: "intelligence"},
{name: "perception", attribute: "wisdom"},
{name: "performance", attribute: "charisma"},
{name: "persuasion", attribute: "charisma"},
{name: "religion", attribute: "intelligence"},
{name: "sleightOfHand", attribute: "dexterity"},
{name: "stealth", attribute: "dexterity"},
{name: "survival", attribute: "wisdom"},
];
var skills = {};
_.each(allSkills, skill => {
var value = signedString(
Characters.calculate.skillMod(charId, skill.name)
);
var prof = Characters.calculate.proficiency(charId, skill.name);
var name = skill.name.charAt(0).toUpperCase() + skill.name.slice(1);
skills[name] = value;
skills[name + "Proficiency"] = prof;
});
return skills;
};
*/
var getLanguages = function(charId){
return Proficiencies.find({
@@ -218,10 +238,6 @@ var getLanguages = function(charId){
}).map(l => l.name).join(", ");
};
var getClasses = function(charId){
return Classes.find({charId}).map(c => `${c.name} ${c.level}`).join(", ");
};
var getAttacks = function(charId){
var attacks = {};
var i = 1;

View File

@@ -0,0 +1,224 @@
getCharacterForComputation = function(charId){
const character = Characters.findOne(charId);
const classes = Classes.find({charId}).fetch();
const effects = Effects.find({charId, enabled: true}).fetch();
const proficiencies = Proficiencies.find({
charId,
enabled: true,
type: {$in: ["skill", "save"]},
}).fetch();
return {character, classes, effects, proficiencies};
}
computeCharacter = function({character, classes, effects, proficiencies}){
var charId = character._id;
let computedClasses = computeCharacterClasses(charId, classes);
let changed = false;
computedCharacter = {};
let i;
for (i = 0; i < 15; i++){
[computedCharacter, changed] = compute({
classes: computedClasses,
oldChar: computedCharacter,
charId,
character,
effects,
proficiencies,
});
if (!changed) break;
}
return computedCharacter;
};
var ensureCharacterExists = (character) => {
if (!character) {
throw new Meteor.Error("Character doesn't exist",
"You can't recompute a character that doesn't exist");
}
};
var ensureWritePermissions = (character, userId) => {
if (
userId &&
userId !== character.owner &&
!_.contains(character.writers, userId)
){
throw new Meteor.Error("Character write denied",
"You don't have permission to recompute this character");
}
};
var computeCharacterClasses = function(charId, classes){
let computedClasses = {};
_.each(classes, (cls) => {
if (computedClasses[cls.name]){
computedClasses[cls.name].level += cls.level;
} else {
computedClasses[cls.name] = cls;
}
});
return computedClasses;
}
var compute = function({
charId, oldChar, character, classes, effects, proficiencies,
}){
let newChar = {};
_.each(effects, (effect, index) => {
if (!effect.stat || effect.operation === "conditional") return;
if (!newChar[effect.stat]) newChar[effect.stat] = defaultStat();
let value = effect.calculation ?
computeEffect(effect.calculation, oldChar, classes) :
effect.value || 0;
let stat = newChar[effect.stat];
if (!_.isNumber(value)) return;
switch (effect.operation) {
case "base":
if (value > stat.base) stat.base = value;
break;
case "proficiency":
if (value > stat.proficiency) stat.proficiency = value;
break;
case "add":
stat.add += value;
break;
case "mul":
stat.mul *= value;
break;
case "min":
if (value > stat.min) stat.min = value;
break;
case "max":
if (value < stat.max) stat.max = value;
break;
case "advantage":
stat.advantage++;
break;
case "disadvantage":
stat.disadvantage++;
break;
case "passiveAdd":
stat.passiveAdd += value;
break;
case "fail":
stat.fail = true;
break;
}
});
_.each(proficiencies, proficiency => {
if (!proficiency.name) return;
if (!newChar[proficiency.name]) newChar[proficiency.name] = defaultStat();
let stat = newChar[proficiency.name];
let value = proficiency.value;
if (value > stat.proficiency) stat.proficiency = value;
});
let changed = false;
_.each(ATTRIBUTES, function(statName) {
if (!newChar[statName]) newChar[statName] = defaultStat();
let stat = newChar[statName];
stat.value = (stat.base + stat.add) * stat.mul;
if (stat.value < stat.min) stat.value = stat.min;
if (stat.value > stat.max) stat.value = stat.max;
if (!_.isEqual(stat.value, oldChar[statName] && oldChar[statName].value)){
changed = true;
}
});
_.each(ALL_SKILLS, function(statName) {
if (!newChar[statName]) newChar[statName] = defaultStat();
let stat = newChar[statName];
stat.value = characterAbilityMod(
oldChar, character[statName] && character[statName].ability
);
stat.value += stat.base + stat.add;
stat.value += stat.proficiency *
characterFieldValue(oldChar, "proficiencyBonus");
stat.value *= stat.mul;
if (stat.value < stat.min) stat.value = stat.min;
if (stat.value > stat.max) stat.value = stat.max;
if (!_.isEqual(stat.value, oldChar[statName] && oldChar[statName].value)){
changed = true;
}
});
return [newChar, changed];
};
var defaultStat = function(){
return {
base: 0,
proficiency: 0,
add: 0,
mul: 1,
min: Number.NEGATIVE_INFINITY,
max: Number.POSITIVE_INFINITY,
advantage: 0,
disadvantage: 0,
passiveAdd: 0,
fail: false,
}
}
var computeEffect = function(string, character, classes){
if (!string) return string;
string = string.replace(/\b[a-z][\w]+/gi, function(sub){
//fields
if (character[sub]){
return characterFieldValue(character, sub);
}
//ability modifiers
if (_.contains(ABILITY_MODS, sub)){
var slice = sub.slice(0, -3);
return getMod(
character[slice] ? characterFieldValue(character, slice) : 0
);
}
//class levels
if (/\w+levels?\b/gi.test(sub)){
//strip out "level"
var className = sub.replace(/levels?\b/gi, "");
return characterClassLevel(classes, className)
}
//character level
if (sub.toUpperCase() === "LEVEL"){
return characterTotalLevel(classes);
}
// exclude math functions
if (math[sub]){
return sub;
}
return 0;
});
try {
var result = math.eval(string);
return result;
} catch (e){
return string;
}
};
var characterFieldValue = function(character, field){
if (_.isNumber(character[field] && character[field].value)){
return character[field].value;
} else {
return field;
}
};
var characterClassLevel = function(classes, className){
if (_.isNumber(classes[className] && classes[className].level)){
return classes[className].level;
} else {
return className;
}
};
var characterTotalLevel = function(classes){
return _.reduce(classes, (memo, cls) => memo + cls.level, 0);
};
var characterAbilityMod = function(character, abilityName){
if (_.isNumber(character[abilityName] && character[abilityName].value)){
return getMod(character[abilityName].value);
} else {
return 0;
}
};

View File

@@ -1,7 +1,7 @@
var childSchema = new SimpleSchema({
parent: {type: Object},
"parent.collection": {type: String},
"parent.id": {type: String, regEx: SimpleSchema.RegEx.Id},
"parent.id": {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
"parent.group": {type: String, optional: true},
"removedWith": {
optional: true,

1901
rpg-docs/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,10 @@
"dependencies": {
"@polymer/polymer": "^1.2.5-npm-test.2",
"babel-runtime": "^6.23.0",
"bcrypt": "^1.0.2",
"bower": "^1.7.9"
"bcrypt": "^1.0.3",
"bower": "^1.7.9",
"core-js": "^2.5.1",
"meteor-node-stubs": "^0.3.2",
"qrcode": "^1.2.0"
}
}

View File

@@ -0,0 +1,45 @@
<!--
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<link rel="import" href="../../../components/polymer/polymer.html">
<link rel="import" href="../../../components/neon-animation/neon-animation-behavior.html">
<link rel="import" href="../../../components/neon-animation/web-animations.html">
<script>
Polymer({
is: 'fade-in-slide-from-left-animation',
behaviors: [
Polymer.NeonAnimationBehavior
],
configure: function(config) {
var node = config.node;
this._effect = new KeyframeEffect(node, [
{'transform': 'translateX(-100%)', 'opacity': '0'},
{'transform': 'translateX(-50%)', 'opacity': '0'},
{'transform': 'none', 'opacity': '1'}
], this.timingFromConfig(config));
if (config.transformOrigin) {
this.setPrefixedProperty(node, 'transformOrigin', config.transformOrigin);
} else {
this.setPrefixedProperty(node, 'transformOrigin', '0 50%');
}
return this._effect;
}
});
</script>

View File

@@ -0,0 +1,45 @@
<!--
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<link rel="import" href="../../../components/polymer/polymer.html">
<link rel="import" href="../../../components/neon-animation/neon-animation-behavior.html">
<link rel="import" href="../../../components/neon-animation/web-animations.html">
<script>
Polymer({
is: 'fade-in-slide-from-right-animation',
behaviors: [
Polymer.NeonAnimationBehavior
],
configure: function(config) {
var node = config.node;
this._effect = new KeyframeEffect(node, [
{'transform': 'translateX(100%)', 'opacity': '0'},
{'transform': 'translateX(50%)', 'opacity': '0'},
{'transform': 'none', 'opacity': '1'}
], this.timingFromConfig(config));
if (config.transformOrigin) {
this.setPrefixedProperty(node, 'transformOrigin', config.transformOrigin);
} else {
this.setPrefixedProperty(node, 'transformOrigin', '0 50%');
}
return this._effect;
}
});
</script>

View File

@@ -0,0 +1,45 @@
<!--
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<link rel="import" href="../../../components/polymer/polymer.html">
<link rel="import" href="../../../components/neon-animation/neon-animation-behavior.html">
<link rel="import" href="../../../components/neon-animation/web-animations.html">
<script>
Polymer({
is: 'fade-out-slide-left-animation',
behaviors: [
Polymer.NeonAnimationBehavior
],
configure: function(config) {
var node = config.node;
this._effect = new KeyframeEffect(node, [
{'transform': 'none', 'opacity': '1'},
{'transform': 'translateX(-50%)', 'opacity': '0'},
{'transform': 'translateX(-100%)', 'opacity': '0'},
], this.timingFromConfig(config));
if (config.transformOrigin) {
this.setPrefixedProperty(node, 'transformOrigin', config.transformOrigin);
} else {
this.setPrefixedProperty(node, 'transformOrigin', '0 50%');
}
return this._effect;
}
});
</script>

View File

@@ -0,0 +1,45 @@
<!--
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<link rel="import" href="../../../components/polymer/polymer.html">
<link rel="import" href="../../../components/neon-animation/neon-animation-behavior.html">
<link rel="import" href="../../../components/neon-animation/web-animations.html">
<script>
Polymer({
is: 'fade-out-slide-right-animation',
behaviors: [
Polymer.NeonAnimationBehavior
],
configure: function(config) {
var node = config.node;
this._effect = new KeyframeEffect(node, [
{'transform': 'none', 'opacity': '1'},
{'transform': 'translateX(50%)', 'opacity': '1'},
{'transform': 'translateX(100%)', 'opacity': '0'}
], this.timingFromConfig(config));
if (config.transformOrigin) {
this.setPrefixedProperty(node, 'transformOrigin', config.transformOrigin);
} else {
this.setPrefixedProperty(node, 'transformOrigin', '0 50%');
}
return this._effect;
}
});
</script>

View File

@@ -0,0 +1,48 @@
{
"name": "paper-stepper",
"version": "2.0-beta.5",
"authors": [
"Zecat <jullienfelix@gmail.com>"
],
"description": "Material paper-stepper element.",
"keywords": [
"web-component",
"polymer",
"seed"
],
"main": "paper-stepper.html",
"license": "http://polymer.github.io/LICENSE.txt",
"homepage": "https://github.com/zecat/paper-stepper/",
"ignore": [
"/.*",
"/test/"
],
"dependencies": {
"polymer": "Polymer/polymer#^1.2.0",
"paper-button": "PolymerElements/paper-button#^1.0.11",
"iron-icons": "PolymerElements/iron-icons#^1.1.3",
"paper-styles": "PolymerElements/paper-styles#^1.1.4",
"paper-ripple": "PolymerElements/paper-ripple#^1.0.5",
"iron-selector": "PolymerElements/iron-selector#^1.3.0",
"iron-icon": "PolymerElements/iron-icon#^1.0.8",
"iron-flex-layout": "PolymerElements/iron-flex-layout#^1.3.1",
"neon-animation": "PolymerElements/neon-animation#^1.1.1",
"iron-validatable-behavior": "PolymerElements/iron-validatable-behavior#^1.0.5",
"iron-collapse": "PolymerElements/iron-collapse#^1.2.0"
},
"devDependencies": {
"paper-input": "PolymerElements/paper-input#^1.1.10",
"paper-material": "PolymerElements/paper-material#^1.0.6",
"iron-component-page": "PolymerElements/iron-component-page#^1.0.0",
"web-component-tester": "*",
"iron-form": "PolymerElements/iron-form#^1.0.16",
"iron-demo-helpers": "PolymerElements/iron-demo-helpers#^1.2.2",
"paper-toggle-button": "PolymerElements/paper-toggle-button#^1.2.0",
"app-layout": "PolymerElements/app-layout#^0.10.2",
"paper-menu": "PolymerElements/paper-menu#^1.2.2",
"iron-scroll-spy": "Zecat/iron-scroll-spy#^2.1.0",
"paper-item": "PolymerElements/paper-item#^1.2.1",
"paper-toast": "PolymerElements/paper-toast#^1.3.0",
"paper-checkbox": "PolymerElements/paper-checkbox#^1.4.0"
}
}

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 225 126" enable-background="new 0 0 225 126" xml:space="preserve">
<g id="background" display="none">
<rect display="inline" fill="#B0BEC5" width="225" height="126"/>
</g>
<g id="label">
</g>
<g id="art">
<g id="ic_x5F_add_x0D_">
</g>
<circle cx="78" cy="98" r="4"/>
<circle cx="171" cy="90" r="4"/>
<circle cx="132" cy="61" r="4"/>
<circle cx="53" cy="61" r="4"/>
<circle cx="126" cy="28" r="4"/>
<circle cx="91" cy="67" r="4"/>
<circle cx="132" cy="90" r="4"/>
<circle cx="65" cy="32" r="4"/>
<path d="M77.7,99.4L51.9,61.1L64.3,31l64.1-4.2L92.6,66.7l16.1,9l23.3-16L174,91h-42.3l-23-12.9L77.7,99.4z M54.1,60.9l24.1,35.7
L106.8,77l-17.4-9.8l34.2-38.1L65.7,33L54.1,60.9z M132.3,89H168l-36-26.8l-21.4,14.6L132.3,89z"/>
</g>
<g id="Guides">
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,424 @@
<link rel="import" href="../../components/iron-validatable-behavior/iron-validatable-behavior.html">
<link rel="import" href="../../components/paper-item/paper-item-behavior.html">
<link rel="import" href="../../components/paper-behaviors/paper-ripple-behavior.html">
<link rel="import" href="../../components/neon-animation/neon-animatable-behavior.html">
<link rel="import" href="../../components/paper-styles/default-theme.html">
<link rel="import" href="step-horizontal-label.html">
<link rel="import" href="step-vertical.html">
<link rel="import" href="../../components/polymer/polymer.html">
<!--
Missing Doc
@element paper-step
@demo demo/index.html
@hero hero.svg
-->
<dom-module id="paper-step">
<template>
<style>
:host {
display: block;
box-sizing: border-box;
outline: 0;
}
:host(:not([vertical])) {
overflow: hidden;
@apply(--layout);
@apply(--layout-flex);
}
:host(:not([vertical])[opened]){
overflow: visible;
}
:host(:not([opened]):not(.neon-animating)) #slideshowViewport{
display: none;
}
#slideshowViewport {
position: absolute;
bottom: 0;
left: 0;
right: 0;
overflow: hidden;
}
#contentWrapper {
padding: 16px 0 16px 16px;
@apply(--layout-scroll);
@apply(--layout-fit);
pointer-events: auto;
}
:host(:focus:not([opened]):not([saved])) step-horizontal-label ::content #badge {
background: var(--paper-step-selectable-hovered-badge-background, --paper-grey-500);
}
</style>
<!-- horizontal step layout -->
<template is="dom-if" if="[[!vertical]]">
<step-horizontal-label id="horizontalStepLabel" editable="[[editable]]" label="[[label]]"
alternative-label="[[_alternativeLabel]]" optional="[[optional]]"
opened="[[opened]]" selectable="[[selectable]]" index="[[index]]"
saved="[[saved]]" stepper-data="[[_stepperData]]">
</step-horizontal-label>
<div id="slideshowViewport">
<div id="contentWrapper">
</div>
</div>
</template>
<!-- vertical step layout -->
<template is="dom-if" if="[[vertical]]">
<step-vertical id="verticalStepLabel" editable="[[editable]]" label="[[label]]"
optional="[[optional]]" opened="[[opened]]"
selectable="[[selectable]]" stepper-data="[[_stepperData]]" index="[[index]]"
saved="[[saved]]" _attr-for-primary-button-text="[[_attrForPrimaryButtonText]]"
can-skip="[[_canSkip]]" has-back-step="[[_hasBackStep]]">
</step-vertical>
</template>
</template>
<script>
Polymer({
is: 'paper-step',
behaviors: [
Polymer.IronValidatableBehavior,
Polymer.NeonAnimatableBehavior,
Polymer.PaperItemBehavior,
Polymer.PaperRippleBehavior
],
/**
* Fired when the step has been saved.
*
* @event paper-step-saved
*/
/**
* Fired when the step has been updated
*
* @event paper-step-updated
*/
properties: {
/**
* MISSING Doc
*/
saved: {
type: Boolean,
value: false,
notify: true,
readOnly: true
},
/**
* Missing Doc
*/
editable: {
type: Boolean,
value: false
},
/**
* Missing Doc
*/
index: {
type: Number,
notify: true,
readOnly: true
},
/**
* Missing Doc
*/
_previousSaved: {
type: Boolean,
readOnly: true
},
/**
* Missing Doc
*/
optional: {
type: Boolean,
value: false
},
/**
* Missing Doc
*/
selectable: {
type: Boolean,
computed: '_computeSelectable(_stepperData.linear, saved, editable, _previousSaved)',
reflectToAttribute: true,
notify: true
},
/**
* Missing Doc
*/
disabled: {
computed: '_computeDisabled(selectable)'
},
/**
* Missing Doc
*/
label: {
type: String,
value: ''
},
/**
* Missing Doc
*/
opened: {
type: Boolean,
value: false
},
/**
* Missing Doc
*/
animationConfig: {
readOnly: true
},
/**
* Missing Doc
*/
entryAnimation: {
readOnly: true
},
/**
* Missing Doc
*/
exitAnimation: {
readOnly: true
},
/**
* Missing Doc
*/
vertical: {
type: Boolean,
readOnly: true,
reflectToAttribute: true
},
/**
* Missing Doc
*/
horizontalHigherEntryAnimation: {
type: String
},
/**
* Missing Doc
*/
horizontalHigherExitAnimation: {
type: String
},
/**
* Missing Doc
*/
horizontalLowerEntryAnimation: {
type: String
},
/**
* Missing Doc
*/
horizontalLowerExitAnimation: {
type: String
},
/**
* Missing Doc
*/
_alternativeLabel: {
type: Boolean,
readOnly: true
},
/**
* Missing Doc
*/
_optionalText: {
type: Boolean,
readOnly: true
},
/**
* Missing Doc
*/
_attrForPrimaryButtonText: {
type: String,
readOnly: true
},
/**
* A reference to the parent stepper
*/
_stepper: {
type: Object,
readOnly: true
},
_stepperData: {
type: Object,
readOnly: true
},
_canSkip: {
type: Boolean,
readOnly: true
},
_hasBackStep: {
type: Boolean,
readOnly: true
}
},
listeners: {
'paper-step-vertical-skip-tapped': 'skip',
'paper-step-vertical-back-tapped': 'back',
'paper-step-vertical-continue-tapped': 'continue',
'tap': '_tapHandler'
},
observers: [
'_toggleClassPosition(index, _stepperData.stepNumber, vertical)',
'_updateSlideshowViewportTop(optional, _alternativeLabel, vertical)',
'_verticalChange(vertical)',
'_focusedChanged(receivedFocusFromKeyboard)',
'_labelElementChanged(_labelElement)'
],
_focusedChanged: function(receivedFocusFromKeyboard) {
if (receivedFocusFromKeyboard) {
this.ensureRipple();
// generate a ripple effect from the center of the badge
var badge = Polymer.dom(this._rippleContainer).querySelector('#badge');
var badgePos = badge.getBoundingClientRect();
var rippleX = badgePos.left + 12;
var rippleY = badgePos.top + 12;
this._ripple.downAction({detail: {x: rippleX, y: rippleY}});
}
if (this.hasRipple()) {
this._ripple.holdDown = receivedFocusFromKeyboard;
}
},
_tapHandler: function(e) {
var rootTarget = Polymer.dom(e).rootTarget;
if (rootTarget !== this && rootTarget !== this._rippleContainer) {
e.stopImmediatePropagation();
}
},
/**
* Missing Doc
*/
skip: function() {
// would it be better to send an event?
this._stepper.progress();
},
/**
* Missing Doc
*/
back: function() {
// would it be better to send an event?
this._stepper.back();
},
/**
* Mark the step as saved and fire `paper-step-saved` if it is valid.
* @return {Boolean} True if the step has been saved.
*/
save: function() {
if ((!this.saved || this.editable) && this.validate()) {
if (this.saved) {
this.fire('paper-step-updated');
} else {
this._setSaved(true);
this.fire('paper-step-saved');
}
return true;
}
return false;
},
/**
* Atempte to save the step and select and to progress into the stepper.
* @return {Boolean} True if the step is valid for saving.
*/
continue: function() {
if (this.save()) {
// would it be better to send an event?
this._stepper.progress();
return true;
}
return false;
},
_removeAnimatingClass: function() {
if (this._animationCanceled) {
this._set_animationCanceled(false);
} else {
this.toggleClass('neon-animating', false);
}
},
_cancelAnimation: function() {
this._set_animationCanceled(true);
this.toggleClass('neon-animating', false);
},
_updateSlideshowViewportTop: function(optional, _alternativeLabel, vertical) {
if (!vertical) {
this.async(function() {
this.$$('#slideshowViewport').style.top = this.clientHeight+'px';
this.fire('step-horizontal-label-resize');
})
}
},
_toggleClassPosition: function(index, stepNumber, vertical) {
this.async(function() {
var stepLabel = this.$$(vertical ? '#verticalStepLabel' : '#horizontalStepLabel');
this.toggleClass('first-step', (index == 1), stepLabel);
this.toggleClass('last-step', (index == stepNumber), stepLabel);
});
},
_updateAnimationConfig: function() {
/* TODO: call this method when horizontalHigher/LowerEntry/ExitAnimation change */
var animatedNode = this.$$('#contentWrapper');
this._setAnimationConfig({
'higher-step-entry': {
node: animatedNode,
name: this.horizontalHigherEntryAnimation
},
'higher-step-exit': {
node: animatedNode,
name: this.horizontalHigherExitAnimation
},
'lower-step-entry': {
node: animatedNode,
name: this.horizontalLowerEntryAnimation
},
'lower-step-exit': {
node: animatedNode,
name: this.horizontalLowerExitAnimation
}
});
},
_verticalChange: function(vertical) {
this.async(function() {
// move or create the content tag
Polymer.dom(this.$$(vertical ? '#verticalStepLabel' : '#contentWrapper')).appendChild(this.$$('content') || this.create('content'));
// reset the ripple
this._ripple = false;
this._rippleContainer = vertical ? this.$$('#verticalStepLabel').$.label : this.$$('#horizontalStepLabel').$.label;
if (!vertical) {
this._updateAnimationConfig();
}
}.bind(this));
},
_computeSelectable: function(linear, saved, editable, previousSaved) {
// TODO: factorize the expression
return (!linear || previousSaved) && (!saved || editable) || (editable && saved);
},
_computeDisabled: function(selectable) {
// disabled is used by IronMenuBehavior in paper-stepper
// TODO: remove selectable attribute and use disabled instead
return !selectable;
}
});
</script>
</dom-module>

View File

@@ -0,0 +1,735 @@
<link rel="import" href="../../components/iron-flex-layout/iron-flex-layout.html">
<link rel="import" href="../../components/iron-selector/iron-selectable.html">
<link rel="import" href="../../components/iron-menu-behavior/iron-menu-behavior.html">
<link rel="import" href="../../components/neon-animation/neon-animation-runner-behavior.html">
<link rel="import" href="../../components/paper-button/paper-button.html">
<link rel="import" href="../../components/paper-styles/default-theme.html">
<link rel="import" href="../../components/paper-styles/shadow.html">
<link rel="import" href="../../components/iron-collapse/iron-collapse.html">
<link rel="import" href="../../components/iron-resizable-behavior/iron-resizable-behavior.html">
<link rel="import" href="../../components/polymer/polymer.html">
<link rel="import" href="animations/fade-in-slide-from-right-animation.html">
<link rel="import" href="animations/fade-out-slide-right-animation.html">
<link rel="import" href="animations/fade-in-slide-from-left-animation.html">
<link rel="import" href="animations/fade-out-slide-left-animation.html">
<link rel="import" href="paper-step.html">
<!--
Missing Doc
## Styling
The default color values respect the material specifications. For basic configuration, just defines the `--primary-color` for your app.
The stepper has a complexe interface so many custom properties are available, use them this way :
```html
<style is="custom-style">
paper-stepper {
--paper-stepper-custom-property: value;
}
paper-step {
--paper-step-custom-property: value;
}
</style>
<paper-stepper>
<paper-step></paper-step>
<paper-step></paper-step>
</paper-stepper>
```
Note, for sizing the stepper and steps depending on the layout, use the following instead of setting 'height' and 'max-height' :
- `--paper-stepper-horizontal-opened-height`
- `--paper-stepper-vertical-max-height`
- `--paper-vertical-step-max-height`
### Stepper
Custom property | Description | Default
------|-------------|----------
`--paper-stepper-horizontal-opened-height` | The horizontal opened stepper height | 450px
`--paper-stepper-horizontal-opening-transition-duration`| The horizontal stepper opening transition duration (ms) | 500
`--paper-stepper-vertical-max-height`| The vertical stepper max height so it has a scrollable area | undefined
### Step
Custom property | Description | Default
------|-------------|----------
`--paper-step-connector-line-background` | The connector lines background color. | `--divider-color`
`--paper-step-label-hover-background` | The steps label background when hovered. | rgba(0,0,0,0.04)
`--paper-step-ink-color` | The steps ripple effect ink color. | `--divider-color`
### Step text label
Custom property | Description | Default
------|-------------|----------
`--paper-step-disabled-label-text-color` | The no-selectable steps label text color. | `--paper-grey-400`
`--paper-step-selectable-label-text-color` | The selectable steps label text color. | `--paper-grey-600`
`--paper-step-opened-label-text-color` | The opened steps label text color. | `--light-theme-text-color`
`--paper-step-label-text-font-size` | The steps label text font size. | 14px
`--paper-step-label-optional-text-font-size` | The steps label optional text font size. | 12px
### Step badge
Custom property | Description | Default
------|-------------|----------
`--paper-step-badge-background` | The badge background. | `--paper-grey-300`
`--paper-step-badge-color` | The badge color. | `--dark-theme-text-color`
`--paper-step-badge-icon-width` | The badge icon width. | 16px
`--paper-step-badge-icon-height` | The badge icon height. | 16px
`--paper-step-opened-badge-background` | The opened steps badge background. | `--primary-color`
`--paper-step-saved-badge-background` | The saved steps badge background. | `--primary-color`
`--paper-step-selectable-hovered-badge-background` | The no-opened no-saved selectable steps badge background. | `--paper-grey-500`
### Vertical step
Custom property | Description | Default
------|-------------|----------
`--paper-vertical-step-continue-button-background` | The continue button background. | `--primary-color`
`--paper-vertical-step-continue-button-color` | The continue button background. | `--dark-theme-text-color`
`--paper-vertical-step-max-height` | The unrolled step content max-height. | 400px
`--paper-vertical-step-transition-duration` | The step opening transition duration | 500ms
@element paper-stepper
@demo demo/index.html
@hero hero.svg
-->
<dom-module id="paper-stepper">
<template>
<style>
:host {
display: block;
box-sizing: border-box;
@apply(--layout-vertical);
@apply(--shadow-elevation-2dp);
background: white;
border-radius: 1px;
}
#verticalResponsiveWidth {
width: var(--paper-stepper-vertical-responsive-width, 0px);
visibility: hidden;
}
/* Horizontal layout styles */
:host([vertical]) {
padding: 4px 0;
@apply(--layout-scroll);
max-height: var(--paper-stepper-vertical-max-height);
}
:host(:not([vertical])) {
position: relative;
/* Note: this variable is updated by the stepper itself, don't use it on user side */
height: var(--label-wrapper-height);
max-height: var(--label-wrapper-height);
transition: max-height var(--paper-stepper-horizontal-opening-transition-duration, 300ms), box-shadow 0.28s cubic-bezier(0.4, 0, 0.2, 1);
}
:host(:not([vertical]):not([opened]):not(.collapsing)) {
@apply(--shadow-none);
}
:host(:not([vertical])[opened]) {
max-height: var(--paper-stepper-horizontal-opened-height, 450px);
}
:host(:not([vertical])[opened]), :host(:not([vertical]).collapsing) {
height: var(--paper-stepper-horizontal-opened-height, 450px);
}
:host(:not([vertical])) #content-wrapper {
@apply(--shadow-elevation-2dp);
@apply(--layout-horizontal);
@apply(--layout-flex-none);
@apply(--layout-justified);
background: var(--paper-stepper-horizontal-label-wrapper-background, --primary-background-color);
border-radius: 1px;
overflow: hidden;
}
.flex {
@apply(--layout-flex);
}
#buttonsWrapper {
@apply(--layout-horizontal);
overflow: hidden;
}
#continueButton:not([disabled]) {
color: var(--primary-color);
}
paper-button[disabled] {
background-color: transparent;
}
[hidden] {
display: none;
}
:host(:not[vertical]) #content-wrapper ::content paper-step[opened] {
overflow: visible;
}
</style>
<!-- Hidden block with width equals to the responsive threshold -->
<div id="verticalResponsiveWidth"></div>
<!-- This wrapper contains the horizontal stepper header or the full vertical stepper -->
<div id="content-wrapper">
<content select="[[selectable]]"></content>
</div>
<!-- Slideshow and buttons area for horizontal layout -->
<template is="dom-if" if="[[!vertical]]">
<div class="flex"></div>
<!--
<div id="buttonsWrapper">
<paper-button hidden$="[[!hasBackButton]]" disabled=[[!_hasBackStep]] on-tap="back">[[backText]]</paper-button>
<span class="flex"></span>
<paper-button hidden$="[[!hasSkipButton]]" disabled="[[!_canSkip]]" on-tap="progress">[[skipText]]</paper-button>
<paper-button id="continueButton" on-tap="continue">{{_choosePrimaryButtonText(_attrForSelectedStepPrimaryButtonText, finishText, continueText, updateText)}}</paper-button>
</div>
-->
</template>
</template>
<script>
'use strict';
Polymer({
is: 'paper-stepper',
behaviors: [
Polymer.IronMenuBehavior,
Polymer.NeonAnimationRunnerBehavior,
Polymer.IronResizableBehavior
],
/**
* Fired when the stepper progress.
*
* @event paper-stepper-progressed
*/
/**
* Fired when all the steps are saved.
*
* @event paper-stepper-completed
*/
properties: {
opened: {
type: Boolean,
computed: '_computeOpened(_selectedIndex)',
observer: '_openedChanged',
notify: true,
reflectToAttribute: true
},
alternativeLabel: {
type: Boolean,
value: false
},
vertical: {
type: Boolean,
value: false,
reflectToAttribute: true
},
backText: {
type: String,
value: 'BACK'
},
finishText: {
type: String,
value: 'FINISH'
},
continueText: {
type: String,
value: 'CONTINUE'
},
skipText: {
type: String,
value: 'SKIP'
},
optionalText: {
type: String,
value: 'Optional'
},
updateText: {
type: String,
value: 'UPDATE'
},
linear: {
type: Boolean,
value: false
},
completed: {
type: Boolean,
value: false,
notify: true,
computed: '_computeCompleted(stepNumber, savedStepNumber)'
},
hasSkipButton: {
type: Boolean,
value: false
},
hasBackButton: {
type: Boolean,
value: false
},
stepNumber: {
type: Number,
notify: true,
computed: '_computeStepNumber(items.length)'
},
savedStepNumber: {
type: Number,
notify: true,
readOnly: true
},
selectedAttribute: {
value: 'opened',
readOnly: true
},
/**
* Note: if you decide to change this attribute, take care to only include `<paper-step>` elements in your `<paper-stepper>`
*/
selectable: {
value: 'paper-step'
},
/**
* Multi mode is not allowed for now in paper-stepper.
*/
mutli: {
value: false,
readOnly: true
},
responsiveCheckFrequence: {
type: Number,
value: 200
},
animateInitialSelection: {
type: Boolean,
value: false
},
horizontalHigherEntryAnimation: {
type: String,
value: 'fade-in-slide-from-right-animation'
},
horizontalHigherExitAnimation: {
type: String,
value: 'fade-out-slide-right-animation'
},
horizontalLowerEntryAnimation: {
type: String,
value: 'fade-in-slide-from-left-animation'
},
horizontalLowerExitAnimation: {
type: String,
value: 'fade-out-slide-left-animation'
},
_skipStepIndex: {
type: Number,
computed: '_compute_skipStepIndex(_selectedIndex)'
},
_canSkip: {
type: Boolean,
notify: true,
computed: '_isntNull(_skipStepIndex)'
},
_backStepIndex: {
type: Number,
computed: '_compute_backStepIndex(_selectedIndex)'
},
_hasBackStep: {
type: Boolean,
computed: '_isntNull(_backStepIndex)'
},
_selectedIndex: {
type: Number,
observer: '_selectedIndexChanged',
readOnly: true,
value: -1
},
_attrForSelectedStepPrimaryButtonText: {
type: String,
computed: '_compute__attrForSelectedStepPrimaryButtonText(_selectedIndex, stepNumber)'
},
_previousAnimatedStep: {
type: Object,
value: null,
readOnly: true
},
_previousSelected: {
type: Object,
readOnly: true
}
},
keyBindings: {
'left': '_onLeftKey',
'right': '_onRightKey'
},
listeners: {
'iron-items-changed': '_initializeSteps',
'paper-step-saved': '_stepSaved',
'transitionend': '_transitionEnd',
'step-horizontal-label-resize': '_updateStepperClosedMaxHeight',
'iron-resize': '_resizeHandler',
'neon-animation-finish': '_onNeonAnimationFinish'
},
observers: [
'_forwardCanSkip(_canSkip, selectedItem)',
'_forwardHasBackStep(_hasBackStep, selectedItem)',
'_forwardVertical(vertical)',
'_forwardAlternativeLabel(alternativeLabel)',
'_forwardStepperData(linear, backText, optionalText, finishText, continueText, skipText, updateText, hasSkipButton, hasBackButton)'
],
attached: function() {
this._responsiveCheck();
},
/**
* Missing Doc
*/
back: function() {
this.selectIndex(this._backStepIndex);
},
/**
* @return {Boolean} Try to continue the current step (if no step opened, use the first one).
*/
continue: function() {
if (this.selectedItem) {
if (this.selectedItem.save()) {
this.progress();
}
}
},
/**
* Loops around the steps from the current (if no step opened, from the first one)
* in order to open the next selectable unsaved step. Returns true if a step has been opened.
*/
progress: function() {
if (!this.stepNumber) {
return false;
}
if (this.completed) {
this.selected = null;
return true;
}
for (var i = (this._selectedIndex+1)%this.stepNumber; i != this._selectedIndex; i = (i+1)%this.stepNumber) {
if (this.items[i].selectable && !this.items[i].saved) {
this.selectIndex(i);
this.fire('paper-stepper-progressed');
return true;
}
}
return false;
},
/* Deselect and set the steps as unsaved*/
reset: function() {
this._setSavedStepNumber(0);
this.selected = null;
if (!this.items.length) {
return;
}
this.items.map(function(step) {
step._setSaved(false);
step._set_previousSaved(false);
});
this.items[0]._set_previousSaved(true);
},
get _isRTL() {
return window.getComputedStyle(this)['direction'] === 'rtl';
},
_onLeftKey: function(event) {
if (this._isRTL) {
this._focusNext();
} else {
this._focusPrevious();
}
event.detail.keyboardEvent.preventDefault();
},
_onRightKey: function(event) {
if (this._isRTL) {
this._focusPrevious();
} else {
this._focusNext();
}
event.detail.keyboardEvent.preventDefault();
},
/**
* Work around: Override the method from IronSelectableBehavior to only allow the selection of selectable step. https://github.com/PolymerElements/iron-selector/issues/99
*/
_selectSelected: function(selected) {
var item = this._valueToItem(this.selected);
if (item) {
var selectable = item.selectable;
if (selectable == undefined) {
// if selectable isn't define it means the step is not yet ready for selection
// and this method will be recalled by the initialization method.
return;
} else if (!selectable) {
//reset previous selected if non null and selectable or deselect
if (this._previousSelected && this._previousSelected.selectable) {
this.selected = this._valueForItem(this.previousSelected);
} else {
this.selected = null;
}
this._set_previousSelected(null);
return;
}
}
this._selection.select(item);
this._set_previousSelected(item);
this._set_selectedIndex(this.indexOf(item));
// Check for items, since this array is populated only when attached
// Since Number(0) is falsy, explicitly check for undefined
if (this.fallbackSelection && this.items.length && (this._selection.get() === undefined)) {
this.selected = this.fallbackSelection;
}
},
_updateStepperClosedMaxHeight: function() {
this.debounce('updateStepperClosedMaxHeight', function() {
this.customStyle['--label-wrapper-height'] = this.$$('#content-wrapper').clientHeight + 'px';
this.updateStyles('--label-wrapper-height');
});
},
_openedChanged: function(newValue, oldValue) {
if (!this.vertical && oldValue != undefined) {
this.toggleClass('collapsing', true);
}
},
_transitionEnd: function(e) {
// check to ignore event fired by paper-ripple
if (e.propertyName == 'max-height') {
this.toggleClass('collapsing', false);
}
},
_computeOpened: function(_selectedIndex) {
return _selectedIndex >= 0;
},
_stepSaved: function(e) {
var previousStep = this.items[this.indexOf(e.target)+1];
if (previousStep) {
previousStep._set_previousSaved(true);
}
this._setSavedStepNumber(this.savedStepNumber+1);
},
_forwardVertical: function(vertical) {
if (this.stepNumber) {
this.items.map(function(step) {
step._setVertical(vertical);
});
}
this.setAttribute('role', vertical ? 'menu': 'menubar');
},
_forwardStepperData: function(linear, backText, optionalText, finishText, continueText, skipText, updateText, hasSkipButton, hasBackButton) {
if (this.stepNumber) {
this.items.map(function(step) {
step._set_stepperData({
linear: linear,
backText: backText,
optionalText: optionalText,
finishText: finishText,
continueText: continueText,
skipText: skipText,
updateText: updateText,
hasSkipButton: hasSkipButton,
hasBackButton: hasBackButton,
stepNumber: this.stepNumber
});
}.bind(this));
}
},
_forwardAlternativeLabel: function(alternativeLabel) {
if (this.stepNumber) {
this.items.map(function(step) {
step._set_alternativeLabel(alternativeLabel);
});
}
},
_computeStepNumber: function(length) {
return length;
},
_selectedIndexChanged: function(newValue, oldValue) {
if (!this.vertical && newValue >=0 && oldValue >= 0) {
var oldStep = this.items[oldValue], newStep = this.items[newValue];
if (newStep.classList.contains('neon-animating')) {
this.cancelAnimation();
}
if (this._previousAnimatedStep && this._previousAnimatedStep.classList.contains('neon-animating')) {
this.cancelAnimation();
this.toggleClass('neon-animating', false, this._previousAnimatedStep);
}
var forward = newValue - oldValue > 0;
this.animationConfig = {
'new-step-entry': {
animatable: newStep,
type: forward ?
newStep.horizontalHigherEntryAnimation && 'higher-step-entry' :
newStep.horizontalLowerEntryAnimation && 'lower-step-entry'
},
'old-step-exit': {
animatable: oldStep,
type: forward ?
oldStep.horizontalLowerExitAnimation && 'lower-step-exit' :
oldStep.horizontalHigherExitAnimation && 'higher-step-exit'
}
};
if (this.animationConfig['new-step-entry'].type) {
this.playAnimation('new-step-entry', {step: newStep});
this.toggleClass('neon-animating', true, newStep);
}
if (this.animationConfig['old-step-exit'].type) {
this.playAnimation('old-step-exit', {step: oldStep});
this.toggleClass('neon-animating', true, oldStep);
}
this._set_previousAnimatedStep(oldStep);
}
},
_onNeonAnimationFinish: function(event) {
var step = event.detail.step;
if (step) {
this.toggleClass('neon-animating', false, step);
}
},
_forwardCanSkip: function(_canSkip, selectedItem) {
selectedItem._set_canSkip(_canSkip);
},
_forwardHasBackStep: function(_hasBackStep, selectedItem) {
selectedItem._set_hasBackStep(_hasBackStep);
},
_compute__attrForSelectedStepPrimaryButtonText: function(selectedIndex) {
/* TODO: compute from selectedItem when https://github.com/PolymerElements/iron-selector/issues/118 is fixed*/
if (selectedIndex < 0) {
return null;
}
var _attrForPrimaryButtonText = this.selectedItem.saved ? 'updateText' :
( (this.stepNumber - this.savedStepNumber) == 1 ? 'finishText' : 'continueText' );
this.selectedItem._set_attrForPrimaryButtonText(_attrForPrimaryButtonText);
return _attrForPrimaryButtonText;
},
_initializeSteps: function() {
var savedStepNumber = 0;
var data = {
linear: this.linear,
backText: this.backText,
optionalText: this.optionalText,
finishText: this.finishText,
continueText: this.continueText,
skipText: this.skipText,
updateText: this.updateText,
hasSkipButton: this.hasSkipButton,
hasBackButton: this.hasBackButton,
stepNumber: this.stepNumber
};
this.items.map(function(step, i) {
if (this.horizontalHigherEntryAnimation && !step.horizontalHigherEntryAnimation) {
step.horizontalHigherEntryAnimation = this.horizontalHigherEntryAnimation;
}
if (this.horizontalHigherExitAnimation && !step.horizontalHigherExitAnimation) {
step.horizontalHigherExitAnimation = this.horizontalHigherExitAnimation;
}
if (this.horizontalLowerEntryAnimation && !step.horizontalLowerEntryAnimation) {
step.horizontalLowerEntryAnimation = this.horizontalLowerEntryAnimation;
}
if (this.horizontalLowerExitAnimation && !step.horizontalLowerExitAnimation) {
step.horizontalLowerExitAnimation = this.horizontalLowerExitAnimation;
}
step._setIndex(i + 1);
step._set_stepper(this);
step._setVertical(this.vertical);
step._set_alternativeLabel(this.alternativeLabel);
step._set_stepperData(data);
// true for index 0
step._set_previousSaved(!i);
if (step.saved) {
savedStepNumber++;
}
}.bind(this));
this._setSavedStepNumber(savedStepNumber);
// method from IronSelectableBehavior
this._updateSelected();
},
_compute_skipStepIndex: function(_selectedIndex) {
if (_selectedIndex >= 0 && !this.completed) {
for (var i=(_selectedIndex+1)%this.stepNumber; i!=_selectedIndex; i=(i+1)%this.stepNumber) {
if (this.items[i].selectable && !this.items[i].saved) {
return i;
}
}
}
return null;
},
_compute_backStepIndex: function(_selectedIndex) {
if (_selectedIndex >= 0) {
for (var i=_selectedIndex - 1; i >= 0; i--) {
if (this.items[i].selectable) {
return i;
}
}
}
return null
},
_isntNull: function(n) {
return n != null;
},
_computeCompleted: function(savedStepNumber, stepNumber) {
var completed = stepNumber == savedStepNumber;
if (completed) {
this.fire('paper-stepper-completed');
return true;
}
return false;
},
_choosePrimaryButtonText: function(_attrForSelectedStepPrimaryButtonText) {
return this[_attrForSelectedStepPrimaryButtonText];
},
_resizeHandler: function() {
this.debounce('paper-stepper-responsive-check', function() {
this._responsiveCheck();
}, this.responsiveCheckFrequence);
},
_responsiveCheck: function() {
var verticalResponsiveWidth = this.$.verticalResponsiveWidth.clientWidth;
if (verticalResponsiveWidth) {
this.vertical = !(this.clientWidth > verticalResponsiveWidth);
}
}
});
</script>
</dom-module>

View File

@@ -0,0 +1,124 @@
<link rel="import" href="../../components/iron-flex-layout/iron-flex-layout.html">
<link rel="import" href="../../components/iron-icons/iron-icons.html">
<link rel="import" href="../../components/iron-icons/editor-icons.html">
<link rel="import" href="../../components/paper-styles/color.html">
<link rel="import" href="../../components/paper-styles/typography.html">
<link rel="import" href="../../components/paper-styles/default-theme.html">
<link rel="import" href="../../components/polymer/polymer.html">
<link rel="import" href="step-label-behavior.html">
<link rel="import" href="step-label-shared-styles.html">
<dom-module id="step-horizontal-label">
<template>
<style include="step-label-shared-styles">
:host{
overflow: hidden;
}
:host([alternative-label]) {
@apply(--layout);
}
#textWrapper {
@apply(--layout-vertical);
padding-right: 8px;
overflow: hidden;
}
#textLabel, #optional {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
#badge {
margin: 0 8px;
}
#badgeWrapper, #textWrapper {
pointer-events: none;
/* to be above paper-ripple*/
z-index: 1;
}
:host(:not([alternative-label])) #label {
@apply(--layout-horizontal);
@apply(--layout-center);
height: 72px;
}
:host(.firstStep:not([alternative-label])) #badge {
margin-left: 24px;
}
:host(.lastStep:not([alternative-label])) #textWrapper {
padding-right: 24px;
}
:host(:not([alternative-label]):not(.first-step)) #label::before,
:host(:not([alternative-label]):not(.last-step)) #label::after,
:host([alternative-label]) #badgeWrapper::before,
:host([alternative-label]) #badgeWrapper::after {
height: 1px;
min-width: 16px;
background: var(--paper-step-connector-line-color, --divider-color);
@apply(--layout-flex);
content: '';
}
:host([alternative-label].first-step) #badgeWrapper::before,
:host([alternative-label].last-step) #badgeWrapper::after {
visibility: hidden;
}
:host([alternative-label]) #textWrapper{
padding: 0 16px;
@apply(--layout-vertical);
@apply(--layout-center);
}
:host([alternative-label]) #textLabel, :host([alternative-label]) #optional{
text-align: center;
@apply(--layout-self-stretch);
}
:host([alternative-label]) #label{
@apply(--layout-vertical);
@apply(--layout-self-stretch);
padding: 24px 0;
width: 100%;
}
:host([alternative-label]) #badgeWrapper {
@apply(--layout-horizontal);
@apply(--layout-center);
padding-bottom: 16px;
}
:host(.first-step:not([alternative-label])) #label {
padding-left: 16px;
}
:host(.last-step:not([alternative-label])) #label {
padding-right: 16px;
}
</style>
<!-- use a "label" wrapper to use the same shared css rules with step-vertical -->
<div id="label">
<div id="badgeWrapper">
<div id="badge">
<iron-icon hidden$="{{!_computeIsIconBadge(icon)}}" icon="{{icon}}"></iron-icon>
<span hidden$="{{_computeIsIconBadge(icon)}}">{{index}}</span>
</div>
</div>
<div id="textWrapper">
<span id="textLabel">[[label]]</span>
<template is="dom-if" if="[[optional]]">
<span id="optional">[[stepperData.optionalText]]</span>
</template>
</div>
</div>
</template>
<script>
Polymer({
is: 'step-horizontal-label',
behaviors: [
Stepper.StepLabelBehavior
],
properties: {
alternativeLabel: {
type: Boolean,
value: false,
reflectToAttribute: true
}
},
});
</script>
</dom-module>

View File

@@ -0,0 +1,58 @@
<link rel="import" href="../../components/polymer/polymer.html">
<script>
window.Stepper = window.Stepper || {};
/*
* @polymerBehavior Stepper.StepLabelBehavior
*/
Stepper.StepLabelBehavior = {
properties: {
icon: {
type: String,
computed: '_computeIcon(saved, editable)'
},
opened: {
type: Boolean,
reflectToAttribute: true
},
selectable: {
type: Boolean,
reflectToAttribute: true
},
editable: {
type: Boolean,
reflectToAttribute: true,
},
label: {
type: String,
notify: true
},
optional: {
type: Boolean,
notify: true
},
saved: {
type: Boolean,
reflectToAttribute: true
},
index: {
type: Number
},
stepperData: {
type: Object
}
},
_computeIcon: function(saved, editable) {
return saved ? ( editable ? 'editor:mode-edit' : 'done' ) : '';
},
_computeIsIconBadge: function(icon) {
return icon.length > 0;
}
};
</script>

View File

@@ -0,0 +1,74 @@
<link rel="import" href="../../components/iron-flex-layout/iron-flex-layout.html">
<link rel="import" href="../../components/paper-styles/default-theme.html">
<link rel="import" href="../../components/paper-styles/color.html">
<link rel="import" href="../../components/paper-styles/typography.html">
<link rel="import" href="../../components/polymer/polymer.html">
<dom-module id="step-label-shared-styles">
<template>
<style>
:host {
display: block;
box-sizing: border-box;
@apply(--paper-font-common-base);
@apply(--layout-flex);
}
#textWrapper {
color: var(--paper-step-disabled-label-text-color, --paper-grey-400);
}
#textLabel {
font-size: var(--paper-step-label-text-font-size, 14px);
}
#optional{
font-size: var(--paper-step-label-optional-text-font-size, 12px);
}
:host([selectable]) #textWrapper {
color: var(--paper-step-selectable-label-text-color, --paper-grey-600);
}
:host([opened]) #textLabel {
color: var(--paper-step-opened-label-text-color, --light-theme-text-color);
font-weight: 500;
}
#label {
cursor: default;
pointer-events: none;
/* For paper-ripple */
position: relative;
}
:host([selectable]) #label {
cursor: pointer;
pointer-events: auto;
}
#label:hover {
/* using alpha chanel for .connectorLine to grow dark */
background: var(--paper-step-label-hover-background, rgba(0,0,0,0.04));
}
#badge {
width: 24px;
height: 24px;
background: var(--paper-step-badge-background, --paper-grey-300);
border-radius: 50%;
color: var(--paper-step-badge-color, --dark-theme-text-color);
font-size: 12px;
@apply(--layout);
@apply(--layout-center-center);
}
#badge iron-icon {
--iron-icon-height: var(--paper-step-badge-icon-width, 16px);
--iron-icon-width: var(--paper-step-badge-icon-height, 16px);
}
:host([opened]) #badge {
background: var(--paper-step-opened-badge-background, --primary-color);
}
:host([saved]) #badge {
background: var(--paper-step-saved-badge-background, --primary-color);
}
:host([selectable]:not([opened]):not([saved])) #label:hover #badge {
background: var(--paper-step-selectable-hovered-badge-background, --paper-grey-500);
}
paper-ripple {
color: var(--paper-step-ink-color, --paper-grey-400);
}
</style>
</template>
</dom-module>

View File

@@ -0,0 +1,177 @@
<link rel="import" href="../../components/iron-flex-layout/iron-flex-layout.html">
<link rel="import" href="../../components/paper-button/paper-button.html">
<link rel="import" href="../../components/paper-styles/color.html">
<link rel="import" href="../../components/paper-styles/default-theme.html">
<link rel="import" href="../../components/paper-styles/typography.html">
<link rel="import" href="../../components/polymer/polymer.html">
<link rel="import" href="../../components/iron-icons/iron-icons.html">
<link rel="import" href="../../components/iron-icon/iron-icon.html">
<link rel="import" href="../../components/iron-icons/editor-icons.html">
<link rel="import" href="../../components/iron-collapse/iron-collapse.html">
<link rel="import" href="step-label-behavior.html">
<link rel="import" href="step-label-shared-styles.html">
<dom-module id="step-vertical">
<template>
<style include="step-label-shared-styles">
:host {
@apply(--layout-vertical);
}
#connectedBadge, #textWrapper {
pointer-events: none;
/* to be above paper-ripple*/
z-index: 1;
}
#collapse {
--iron-collapse-transition-duration: var(--paper-vertical-step-transition-duration, 500ms);
@apply(--layout-horizontal);
}
/**
* Content
*/
#connectedStep {
@apply(--layout-horizontal);
}
#contentConnectorLine {
width: 1px;
background: var(--divider-color, --paper-grey-300);
margin-left: 36px;
margin-right: 24px;
}
#stepWrapper {
@apply(--layout-flex);
/*should be 48px on large screen?*/
padding-right: 24px;
}
#paperStepWrapper {
max-height: calc(var(--paper-vertical-step-max-height, 400px) - 92px);
@apply(--layout-scroll);
}
/**
* Buttons
*/
#buttonsWrapper {
height: 48px;
@apply(--layout-horizontal);
@apply(--layout-center);
@apply(--layout-flex-none);
}
#buttonsWrapper > paper-button {
margin-right: 8px;
margin-left: 0;
}
#continueButton {
--paper-button: {
background: var(--paper-vertical-step-continue-button-background, --primary-color);
color: var(--paper-vertical-step-continue-button-color, --dark-theme-text-color);
};
}
/**
* Label
*/
#textWrapper {
@apply(--layout-vertical);
padding: 8px 0 8px 8px;
}
#label {
@apply(--layout-horizontal);
}
#connectedBadge {
@apply(--layout-vertical);
@apply(--layout-center);
margin-left: 24px;
}
#beforeConnectorLine, #afterConnectorLine {
width: 1px;
background: var(--paper-step-connector-line-color, --divider-color);
}
#beforeConnectorLine {
height: 10px;
}
#afterConnectorLine {
@apply(--layout-flex);
}
#badge {
margin: 8px 0;
}
:host(.first-step) #beforeConnectorLine, :host(.lastStep) #contentConnectorLine{
visibility: hidden;
}
:host(.last-step:not([opened])) #afterConnectorLine {
display: none;
}
</style>
<div id="label">
<div id="connectedBadge">
<div id="beforeConnectorLine"></div>
<div id="badge">
<iron-icon hidden$="{{!_computeIsIconBadge(icon)}}" icon="{{icon}}"></iron-icon>
<span hidden$="{{_computeIsIconBadge(icon)}}">{{index}}</span>
</div>
<div id="afterConnectorLine" class="connectorLine"></div>
</div>
<div id="textWrapper">
<span id="textLabel">[[label]]</span>
<template is="dom-if" if="[[optional]]">
<span id="optional">[[stepperData.optionalText]]</span>
</template>
</div>
</div>
<iron-collapse id="collapse" opened="[[opened]]">
<div id="contentConnectorLine"></div>
<div id="stepWrapper">
<div id="paperStepWrapper">
<content></content>
</div>
<div id="buttonsWrapper">
<paper-button id="continueButton" on-tap="continue">{{choosePrimaryButtonText(_attrForPrimaryButtonText, stepperData.continueText, stepperData.finishText, stepperData.updateText)}}</paper-button>
<paper-button id="backButton" disabled="[[!hasBackStep]]" on-tap="back" hidden$="[[!stepperData.hasBackButton]]">
[[stepperData.backText]]
</paper-button>
<paper-button id="skipButton" disabled="[[!canSkip]]" on-tap="skip" hidden$="[[!stepperData.hasSkipButton]]">
[[stepperData.skipText]]
</paper-button>
</div>
</div>
</iron-collapse>
</template>
<script>
Polymer({
is: 'step-vertical',
properties: {
canSkip: {
type: Boolean
},
_attrForPrimaryButtonText: {
type: String,
value: false
},
hasBackStep: Boolean
},
behaviors: [
Stepper.StepLabelBehavior
],
skip: function() {
this.fire('paper-step-vertical-skip-tapped');
},
back: function() {
this.fire('paper-step-vertical-back-tapped');
},
continue: function() {
this.fire('paper-step-vertical-continue-tapped');
},
choosePrimaryButtonText: function(_attrForPrimaryButtonText) {
return this.stepperData[_attrForPrimaryButtonText];
}
});
</script>
</dom-module>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -25,3 +25,10 @@ Meteor.publish("characterList", function(){
Parties.find({owner: userId}),
];
});
DDPRateLimiter.addRule({
name: "characterList",
type: "subscription",
userId(){ return true; },
connectionId(){ return true; },
}, 8, 5000);

View File

@@ -35,9 +35,16 @@ Meteor.publish("singleCharacter", function(characterId){
}
});
DDPRateLimiter.addRule({
name: "singleCharacter",
type: "subscription",
userId(){ return true; },
connectionId(){ return true; },
}, 8, 5000);
Meteor.publish("singleCharacterName", function(characterId){
userId = this.userId;
var char = Characters.findOne({
return Characters.find({
_id: characterId,
$or: [
{readers: userId},
@@ -45,8 +52,7 @@ Meteor.publish("singleCharacterName", function(characterId){
{owner: userId},
{"settings.viewPermission": "public"},
],
}, {
fields:{"name": 1}
});
if (char) {
return Characters.find(characterId, {fields:{"name": 1}});
}
});
});

View File

@@ -1,3 +1,8 @@
Meteor.publish("user", function(){
return Meteor.users.find(this.userId, {fields: {roles: 1}});
return Meteor.users.find(this.userId, {fields: {
roles: 1,
username: 1,
profile: 1,
apiKey: 1,
}});
});