Added simple feature UI components and insertion dialog

This commit is contained in:
Stefan Zermatten
2019-02-25 15:38:57 +02:00
parent e5ff116208
commit b7a4a3d3fa
23 changed files with 579 additions and 93 deletions

View File

@@ -47,6 +47,7 @@
import DialogStack from '/imports/ui/dialogStack/DialogStack.Story.vue';
import EffectEdit from '/imports/ui/components/EffectEdit.Story.vue';
import EffectEditExpansionList from '/imports/ui/components/EffectEditExpansionList.Story.vue';
import FeatureCard from '/imports/ui/components/FeatureCard.Story.vue';
import HealthBar from '/imports/ui/components/HealthBar.Story.vue';
import HitDiceListTile from '/imports/ui/components/HitDiceListTile.Story.vue';
import IconSearch from '/imports/ui/components/IconSearch.Story.vue';
@@ -64,6 +65,7 @@
DialogStack,
EffectEdit,
EffectEditExpansionList,
FeatureCard,
HealthBar,
HitDiceListTile,
IconSearch,

View File

@@ -15,6 +15,9 @@
>
<v-tab>
Stats
</v-tab>
<v-tab>
Features
</v-tab>
<v-tab>
Tree
@@ -25,6 +28,9 @@
<v-tabs-items v-model="tab">
<v-tab-item>
<stats-tab :char-id="character._id"/>
</v-tab-item>
<v-tab-item>
<features-tab :char-id="character._id"/>
</v-tab-item>
<v-tab-item>
<character-tree-view :char-id="character._id"/>
@@ -43,6 +49,7 @@
import { mapMutations } from "vuex";
import { theme } from '/imports/ui/theme.js';
import StatsTab from '/imports/ui/character/StatsTab.vue';
import FeaturesTab from '/imports/ui/character/FeaturesTab.vue';
import CharacterTreeView from '/imports/ui/character/CharacterTreeView.vue';
import { recomputeCreature } from '/imports/api/creature/creatureComputation.js'
@@ -53,6 +60,7 @@
},
components: {
StatsTab,
FeaturesTab,
CharacterTreeView,
},
data(){return {

View File

@@ -0,0 +1,73 @@
<template lang="html">
<div class="features">
<column-layout>
<div v-for="feature in features" :key="feature._id">
<feature-card
v-bind="feature"
:data-id="feature._id"
/>
</div>
</column-layout>
<v-btn fixed fab bottom right
color="primary"
@click="insertFeature"
data-id="insert-feature-fab"
>
<v-icon>add</v-icon>
</v-btn>
</div>
</template>
<script>
import Creatures from '/imports/api/creature/Creatures.js';
import Features from '/imports/api/creature/properties/Features.js';
import { insertFeature } from '/imports/api/creature/properties/Features.js';
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
import FeatureCard from '/imports/ui/components/FeatureCard.vue';
import { evaluateComputation, evaluateString } from '/imports/ui/utility/evaluate.js';
export default {
props: {
charId: String,
},
components: {
ColumnLayout,
FeatureCard,
},
meteor: {
features(){
let char = Creatures.findOne(this.charId, {fields: {variables: 1}});
if (!char) return [];
let vars = char.variables;
return Features.find({
charId: this.charId,
}, {
sort: {order: 1},
}).map(f => {
f.uses = evaluateComputation(f.uses, vars);
f.description = evaluateString(f.description, vars);
return f;
});
},
},
methods: {
insertFeature(){
const charId = this.charId;
this.$store.commit('pushDialogStack', {
component: 'feature-creation-dialog',
elementId: 'insert-feature-fab',
callback(feature){
if (!feature) return;
feature.charId = charId;
let featureId = insertFeature.call({feature});
return featureId
}
});
}
}
};
</script>
<style lang="css" scoped>
</style>

View File

@@ -9,16 +9,15 @@
@change="change"
:debounce-time="0"
/>
<div slot="actions">
<v-spacer/>
<v-btn
flat
:disabled="!valid"
@click="$store.dispatch('popDialogStack', attribute)"
>
Insert Attribute
</v-btn>
</div>
<v-spacer slot="actions"/>
<v-btn
flat
slot="actions"
:disabled="!valid"
@click="$store.dispatch('popDialogStack', attribute)"
>
Insert Attribute
</v-btn>
</dialog-base>
</template>
@@ -26,7 +25,6 @@
import AttributeEdit from '/imports/ui/components/AttributeEdit.vue';
import Attributes from '/imports/api/creature/properties/Attributes.js';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import { Tracker } from 'meteor/tracker';
export default {
components: {

View File

@@ -49,7 +49,6 @@
/>
<smart-select
label="Reset"
append-icon="arrow_drop_down"
clearable
:items="resetOptions"
:value="attribute.reset"

View File

@@ -0,0 +1,51 @@
<template lang="html">
<column-layout>
<div
v-for="(feature, index) in features"
:key="index"
>
<feature-card
v-bind="feature"
/>
</div>
</column-layout>
</template>
<script>
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
import FeatureCard from '/imports/ui/components/FeatureCard.vue';
export default {
dontWrap: true,
components: {
ColumnLayout,
FeatureCard,
},
data(){return {
features: [
{
name: 'Feature 1',
enabled: true,
alwaysEnabled: true,
description: `
blah blah, with
spacing
`,
color: '#f44336',
}, {
name: 'Feature 2',
enabled: false,
alwaysEnabled: false,
description: `Short Description`,
uses: 5,
used: 2,
},
],
}},
}
</script>
<style lang="css' scoped>
</style>

View File

@@ -0,0 +1,60 @@
<template lang="html">
<toolbar-card :color="color" @click="$emit('click')">
<span slot="toolbar">
{{name}}
</span>
<v-spacer slot="toolbar"/>
<v-checkbox
hide-details
class="shrink"
v-if="!alwaysEnabled"
:value="enabled"
@change="enabled => $emit('change', {enabled})"
slot="toolbar"
/>
<v-card-text>
{{description}}
</v-card-text>
<v-card-actions v-if="uses">
<v-spacer/>
<v-btn
flat
:disabled="uses - used <= 0"
@click="$emit('used')"
>
Use
</v-btn>
<v-btn
flat
:disabled="!used"
@click="$emit('reset')"
>
Reset
</v-btn>
</v-card-actions>
</toolbar-card>
</template>
<script>
import ToolbarCard from '/imports/ui/components/ToolbarCard.vue';
export default {
props: {
charId: String,
name: String,
description: String,
uses: Number,
used: Number,
reset: String,
color: String,
enabled: Boolean,
alwaysEnabled: Boolean,
},
components: {
ToolbarCard,
},
};
</script>
<style lang="css" scoped>
</style>

View File

@@ -0,0 +1,78 @@
<template lang="html">
<dialog-base>
<div slot="toolbar">
New Feature
</div>
<feature-edit
:feature="feature"
:errors="errors"
@change="change"
:debounce-time="0"
/>
<v-spacer slot="actions"/>
<v-btn
flat
slot="actions"
:disabled="!valid"
@click="$store.dispatch('popDialogStack', feature)"
>
Insert Feature
</v-btn>
</dialog-base>
</template>
<script>
import FeatureEdit from '/imports/ui/components/FeatureEdit.vue';
import Features from '/imports/api/creature/properties/Features.js';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
export default {
components: {
FeatureEdit,
DialogBase,
},
data(){ return {
feature: {
name: 'New Feature',
description: null,
uses: null,
used: 0,
reset: null,
enabled: true,
alwaysEnabled: true,
color: '#9E9E9E',
},
valid: true,
}},
methods: {
change(update, ack){
for (key in update){
this.feature[key] = update[key];
}
if (ack) ack();
},
},
created(){
this.validationContext = Features.simpleSchema().newContext();
},
computed: {
errors(){
this.valid = true;
let cleanAtt = this.validationContext.clean(this.feature)
this.validationContext.validate(cleanAtt, {keys: [
'name', 'description', 'uses', 'used', 'reset', 'enabled',
'alwaysEnabled', 'color',
]});
let errors = {};
this.validationContext.validationErrors().forEach(error => {
if (this.valid) this.valid = false;
errors[error.name] = Features.simpleSchema().messageForError(error);
});
return errors;
},
},
};
</script>
<style lang="css" scoped>
</style>

View File

@@ -0,0 +1,113 @@
<template lang="html">
<div>
<text-field
label="Name"
:value="feature.name"
@change="(name, ack) => $emit('change', {name}, ack)"
:error-messages="errors.name"
:debounce-time="debounceTime"
/>
<text-field
label="Used"
type="number"
:value="feature.used"
@change="(used, ack) => $emit('change', {used}, ack)"
:error-messages="errors.used"
:debounce-time="debounceTime"
/>
<text-field
label="Uses"
:value="feature.uses"
@change="(uses, ack) => $emit('change', {uses}, ack)"
:error-messages="errors.uses"
:debounce-time="debounceTime"
/>
<smart-select
label="Reset"
clearable
:items="resetOptions"
:value="feature.reset"
:error-messages="errors.reset"
:menu-props="{auto: true, lazy: true}"
@change="(reset, ack) => $emit('change', {reset}, ack)"
:debounce-time="debounceTime"
/>
<smart-select
label="Enabled"
:items="enabledOptions"
:value="enabledStatus"
:error-messages="errors.enabled || errors.alwaysEnabled"
:menu-props="{auto: true, lazy: true}"
@change="changeEnabled"
:debounce-time="debounceTime"
/>
<text-area
label="Description"
:value="feature.description"
:error-messages="errors.description"
@change="(description, ack) => $emit('change', {description}, ack)"
:debounce-time="debounceTime"
/>
</div>
</template>
<script>
export default {
props: {
feature: {
type: Object,
default: () => ({}),
},
errors: {
type: Object,
default: () => ({}),
},
debounceTime: Number,
},
data(){ return{
resetOptions: [
{
text: 'Short rest',
value: 'shortRest',
}, {
text: 'Long rest',
value: 'longRest',
}
],
enabledOptions: [
{
text: 'Always enabled',
value: 'always',
}, {
text: 'Enabled',
value: 'enabled',
}, {
text: 'Disabled',
value: 'disabled',
}
],
}},
computed: {
enabledStatus(){
if (!this.feature) return;
if (this.feature.alwaysEnabled) return 'always';
if (this.feature.enabled) return 'enabled';
return 'disabled';
},
},
methods: {
changeEnabled(value, ack){
if (value === 'always'){
this.$emit('change', {enabled: true, alwaysEnabled: true}, ack);
} else if (value === 'enabled'){
this.$emit('change', {enabled: true, alwaysEnabled: false}, ack);
} else if (value === 'disabled'){
this.$emit('change', {enabled: false, alwaysEnabled: false}, ack);
}
}
}
};
</script>
<style lang="css" scoped>
</style>

View File

@@ -0,0 +1,38 @@
<template lang="html">
<v-card>
<v-toolbar
flat
style="transform: none;"
@click="$emit('click')"
:color="color"
:dark="isDark"
>
<slot name="toolbar"/>
</v-toolbar>
<div>
<slot/>
</div>
</v-card>
</template>
<script>
import isDarkColor from '/imports/ui/utility/isDarkColor.js';
export default {
props: {
color: {
type: String,
default(){
return this.$vuetify.theme.secondary;
},
},
},
computed: {
isDark(){
return isDarkColor(this.color);
}
}
};
</script>
<style lang="css" scoped>
</style>

View File

@@ -4,6 +4,7 @@
:loading="loading"
:error-messages="errors"
:value="safeValue"
:auto-grow="autoGrow"
@input="input"
@focus="focused = true"
@blur="focused = false"
@@ -15,5 +16,11 @@
export default {
mixins: [SmartInput],
props: {
autoGrow: {
type: Boolean,
default: true,
},
},
};
</script>

View File

@@ -16,7 +16,7 @@
</template>
</v-toolbar>
<v-card-text id="base-dialog-body" v-scroll:#base-dialog-body="onScroll">
<v-tabs-items :value="isEditing ? 1 : 0">
<v-tabs-items :value="isEditing ? 1 : 0" touchless>
<v-tab-item>
<slot/>
</v-tab-item>

View File

@@ -1,11 +1,13 @@
import AttributeDialog from '/imports/ui/components/AttributeDialog.vue';
import AttributeDialogContainer from '/imports/ui/components/AttributeDialogContainer.vue';
import AttributeCreationDialog from '/imports/ui/components/AttributeCreationDialog.vue';
import FeatureCreationDialog from '/imports/ui/components/FeatureCreationDialog.vue';
import SkillDialogContainer from '/imports/ui/components/SkillDialogContainer.vue';
export default {
AttributeDialog,
AttributeDialogContainer,
AttributeCreationDialog,
FeatureCreationDialog,
SkillDialogContainer,
};

View File

@@ -217,7 +217,7 @@
right: 0;
bottom: 0;
pointer-events: none;
z-index: 3;
z-index: 4;
}
.dialog-sizer {
position: relative;

View File

@@ -0,0 +1,29 @@
// Computations resolve to numbers
// vars is a dict of variables to substitute
function evaluateComputation(string, vars){
if (!string) return string;
// Replace all the string variables with numbers if possible
let substitutedString = string.replace(
/\w*[a-z]\w*/gi,
sub => vars.hasOwnProperty(sub) ? vars[sub] : sub
);
// Evaluate the expression to a number or return it as is.
try {
return math.eval(substitutedString);
} catch (e){
return substitutedString;
}
};
// Strings can have computations in bracers like so: {computation}
// vars is a dict of variables to substitute
function evaluateString(string, vars){
if (!string) return string;
// Compute everything inside bracers
return string.replace(/\{([^\{\}]*)\}/g, function(match, p1){
return evaluateComputation(p1, vars);
});
}
export { evaluateComputation, evaluateString };