Moved UI to client folder to fix HMR
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
<template lang="html">
|
||||
<div
|
||||
v-if="computedErrors.length"
|
||||
class="error-list"
|
||||
>
|
||||
<v-slide-x-transition
|
||||
group
|
||||
hide-on-leave
|
||||
>
|
||||
<v-alert
|
||||
v-for="error in computedErrors"
|
||||
:key="error.message"
|
||||
:value="true"
|
||||
:icon="errorIcon(error.type)"
|
||||
:color="errorColor(error.type)"
|
||||
class="mb-2"
|
||||
dense
|
||||
text
|
||||
>
|
||||
<pre>{{ error.message }}</pre>
|
||||
</v-alert>
|
||||
</v-slide-x-transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
export default {
|
||||
props: {
|
||||
errors: {
|
||||
type: Array,
|
||||
default: undefined,
|
||||
},
|
||||
calculations: {
|
||||
type: Array,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
computedErrors(){
|
||||
if (this.errors) {
|
||||
return this.errors;
|
||||
} else if (this.calculations){
|
||||
let errors = [];
|
||||
this.calculations.forEach(calc => {
|
||||
if (calc.errors) errors.push(...calc.errors)
|
||||
});
|
||||
return errors;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
errorIcon(type){
|
||||
if (type === 'subsitution'){
|
||||
return 'mdi-information';
|
||||
} else if (type === 'evaluation'){
|
||||
return 'mdi-alert-circle';
|
||||
} else {
|
||||
return 'mdi-alert'
|
||||
}
|
||||
},
|
||||
errorColor(type){
|
||||
if (type === 'subsitution'){
|
||||
return 'info';
|
||||
} else if (type === 'evaluation'){
|
||||
return 'warning';
|
||||
} else {
|
||||
return 'error'
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css">
|
||||
.error-list .v-alert__content{
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,57 @@
|
||||
<template lang="html">
|
||||
<div class="computed-field">
|
||||
<text-field
|
||||
:value="model.calculation"
|
||||
v-bind="$attrs"
|
||||
@change="(value, ack) => $emit('change', {path: ['calculation'], value, ack})"
|
||||
>
|
||||
<template
|
||||
v-if="showValue"
|
||||
#value
|
||||
>
|
||||
{{ model.value }}
|
||||
</template>
|
||||
</text-field>
|
||||
<calculation-error-list :errors="errorList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import CalculationErrorList from '/imports/client/ui/properties/forms/shared/CalculationErrorList.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CalculationErrorList,
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
hideValue: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
showValue() {
|
||||
const value = this.model.value;
|
||||
if (
|
||||
this.hideValue ||
|
||||
(value === undefined || value === null) ||
|
||||
value == this.model.calculation
|
||||
) return false;
|
||||
return true;
|
||||
},
|
||||
errorList(){
|
||||
if (this.model.parseError){
|
||||
return [this.model.parseError, ...this.model.errors];
|
||||
} else {
|
||||
return this.model.errors;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,37 @@
|
||||
<template lang="html">
|
||||
<form-sections v-if="standalone">
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-header>
|
||||
{{ name }}
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content class="pt-2">
|
||||
<slot />
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</form-sections>
|
||||
<v-expansion-panel v-else>
|
||||
<v-expansion-panel-header>
|
||||
{{ name }}
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content class="pt-2">
|
||||
<slot />
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import FormSections from '/imports/client/ui/properties/forms/shared/FormSections.vue';
|
||||
export default {
|
||||
components: {
|
||||
FormSections,
|
||||
},
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
standalone: Boolean,
|
||||
},
|
||||
}
|
||||
export { FormSections };
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template lang="html">
|
||||
<v-expansion-panels
|
||||
popout
|
||||
multiple
|
||||
>
|
||||
<slot />
|
||||
</v-expansion-panels>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
export default {}
|
||||
</script>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template lang="html">
|
||||
<div
|
||||
class="d-flex justify-center flex-wrap"
|
||||
>
|
||||
<div class="mx-1">
|
||||
<color-picker
|
||||
label="Color"
|
||||
:value="model.color"
|
||||
@input="value =>$emit('change', {path: ['color'], value})"
|
||||
/>
|
||||
</div>
|
||||
<div class="mx-1">
|
||||
<icon-picker
|
||||
label="Icon"
|
||||
:value="model.icon"
|
||||
:error-messages="errors.icon"
|
||||
@change="(value, ack) =>$emit('change', {path: ['icon'], value, ack})"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import PropertyIcon from '/imports/client/ui/properties/shared/PropertyIcon.vue';
|
||||
import ColorPicker from '/imports/client/ui/components/ColorPicker.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PropertyIcon,
|
||||
ColorPicker,
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
errors: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,52 @@
|
||||
<template lang="html">
|
||||
<div class="inline-computation-field">
|
||||
<text-area
|
||||
:value="model.text"
|
||||
v-bind="$attrs"
|
||||
@change="(value, ack) => $emit('change', {path: ['text'], value, ack})"
|
||||
/>
|
||||
<template v-for="calc in model.inlineCalculations">
|
||||
<div
|
||||
v-if="calc && calc.calculation && (
|
||||
(calc.errors && calc.errors.length) || calc.parseError
|
||||
)"
|
||||
:key="calc.calculation"
|
||||
class="mb-4"
|
||||
>
|
||||
<div
|
||||
class="warning--text mb-2 ml-4"
|
||||
style="font-family: monospace;"
|
||||
>
|
||||
{ {{ calc.calculation }} }
|
||||
</div>
|
||||
<calculation-error-list
|
||||
v-if="calc.parseError"
|
||||
:errors="[calc.parseError]"
|
||||
/>
|
||||
<calculation-error-list
|
||||
v-if="calc.errors && calc.errors.length"
|
||||
:errors="calc.errors"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import CalculationErrorList from '/imports/client/ui/properties/forms/shared/CalculationErrorList.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CalculationErrorList,
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,89 @@
|
||||
<template lang="html">
|
||||
<smart-select
|
||||
append-icon="mdi-menu-down"
|
||||
:clearable="clearable"
|
||||
class="ml-3"
|
||||
v-bind="$attrs"
|
||||
:menu-props="{transition: 'slide-y-transition', lazy: true}"
|
||||
:items="values"
|
||||
:value="value"
|
||||
@change="(value, ack) => $emit('change', value, ack)"
|
||||
>
|
||||
<v-icon
|
||||
slot="prepend"
|
||||
class="icon"
|
||||
:class="iconClass"
|
||||
>
|
||||
{{ displayedIcon }}
|
||||
</v-icon>
|
||||
</smart-select>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import getProficiencyIcon from '/imports/client/ui/utility/getProficiencyIcon.js';
|
||||
|
||||
const ICON_SPIN_DURATION = 300;
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
clearable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data(){ return {
|
||||
displayedIcon: 'mdi-radiobox-blank',
|
||||
iconClass: '',
|
||||
values: [
|
||||
{value: 1, text: 'Proficient'},
|
||||
{value: 0.49, text: 'Half proficiency bonus rounded down'},
|
||||
{value: 0.5, text: 'Half proficiency bonus rounded up'},
|
||||
{value: 2, text: 'Double proficiency bonus'},
|
||||
],
|
||||
}},
|
||||
watch: {
|
||||
'value': {
|
||||
immediate: true,
|
||||
handler(newValue){
|
||||
let newIcon = getProficiencyIcon(newValue);
|
||||
this.iconClass='leaving';
|
||||
setTimeout(() => {
|
||||
this.displayedIcon = newIcon;
|
||||
this.iconClass='arriving';
|
||||
requestAnimationFrame(() => {
|
||||
this.iconClass='';
|
||||
});
|
||||
}, ICON_SPIN_DURATION / 2);
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.theme--light .icon {
|
||||
color: black;
|
||||
}
|
||||
.icon {
|
||||
min-width: 30px;
|
||||
transition: transform 0.15s linear, opacity 0.15s ease;
|
||||
transform-origin: 18px center;
|
||||
margin-left: -12px;
|
||||
}
|
||||
.icon.leaving {
|
||||
transform: translateY(-24px);
|
||||
opacity: 0;
|
||||
}
|
||||
.icon.arriving {
|
||||
transform: translateY(24px);
|
||||
opacity: 0;
|
||||
transition: none;
|
||||
}
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,11 @@
|
||||
import createListOfProperties from '/imports/client/ui/properties/forms/shared/lists/createListOfProperties.js';
|
||||
|
||||
const attributeListMixin = {
|
||||
meteor: {
|
||||
attributeList(){
|
||||
return createListOfProperties({type: {$in: ['attribute', 'skill']}});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default attributeListMixin;
|
||||
@@ -0,0 +1,23 @@
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||
|
||||
export default function createListOfProperties(filter = {}, getNamesWithValues) {
|
||||
filter.removed = { $ne: true };
|
||||
let propertyList = [];
|
||||
let variableNames = new Set();
|
||||
function addUniquePropertys(property) {
|
||||
if (property.variableName && !variableNames.has(property.variableName)) {
|
||||
variableNames.add(property.variableName);
|
||||
propertyList.push({
|
||||
text: property.name || property.variableName,
|
||||
value: property.variableName,
|
||||
propertyType: property.propertyType,
|
||||
});
|
||||
}
|
||||
}
|
||||
let options = { sort: { order: 1, variableName: 1 } }
|
||||
CreatureProperties.find(filter, options).forEach(addUniquePropertys);
|
||||
LibraryNodes.find(filter, options).forEach(addUniquePropertys);
|
||||
if (getNamesWithValues) return propertyList;
|
||||
return Array.from(variableNames);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import createListOfProperties from '/imports/client/ui/properties/forms/shared/lists/createListOfProperties.js'
|
||||
|
||||
const saveListMixin = {
|
||||
meteor: {
|
||||
saveList(){
|
||||
return createListOfProperties({type: 'skill', skillType: 'save'});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default saveListMixin;
|
||||
@@ -0,0 +1,11 @@
|
||||
import createListOfProperties from '/imports/client/ui/properties/forms/shared/lists/createListOfProperties.js'
|
||||
|
||||
const skillListMixin = {
|
||||
meteor: {
|
||||
skillList(){
|
||||
return createListOfProperties({type: 'skill'});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default skillListMixin;
|
||||
@@ -0,0 +1,61 @@
|
||||
import ActionForm from '/imports/client/ui/properties/forms/ActionForm.vue';
|
||||
import AdjustmentForm from '/imports/client/ui/properties/forms/AdjustmentForm.vue';
|
||||
import AttributeForm from '/imports/client/ui/properties/forms/AttributeForm.vue';
|
||||
import BuffForm from '/imports/client/ui/properties/forms/BuffForm.vue';
|
||||
import BuffRemoverForm from '/imports/client/ui/properties/forms/BuffRemoverForm.vue';
|
||||
import BranchForm from '/imports/client/ui/properties/forms/BranchForm.vue';
|
||||
import ClassForm from '/imports/client/ui/properties/forms/ClassForm.vue';
|
||||
import ClassLevelForm from '/imports/client/ui/properties/forms/ClassLevelForm.vue';
|
||||
import ConstantForm from '/imports/client/ui/properties/forms/ConstantForm.vue';
|
||||
import ContainerForm from '/imports/client/ui/properties/forms/ContainerForm.vue';
|
||||
import DamageForm from '/imports/client/ui/properties/forms/DamageForm.vue';
|
||||
import DamageMultiplierForm from '/imports/client/ui/properties/forms/DamageMultiplierForm.vue';
|
||||
import EffectForm from '/imports/client/ui/properties/forms/EffectForm.vue';
|
||||
import FeatureForm from '/imports/client/ui/properties/forms/FeatureForm.vue';
|
||||
import FolderForm from '/imports/client/ui/properties/forms/FolderForm.vue';
|
||||
import ItemForm from '/imports/client/ui/properties/forms/ItemForm.vue';
|
||||
import NoteForm from '/imports/client/ui/properties/forms/NoteForm.vue';
|
||||
import PointBuyForm from '/imports/client/ui/properties/forms/PointBuyForm.vue';
|
||||
import ProficiencyForm from '/imports/client/ui/properties/forms/ProficiencyForm.vue';
|
||||
import ReferenceForm from '/imports/client/ui/properties/forms/ReferenceForm.vue';
|
||||
import RollForm from '/imports/client/ui/properties/forms/RollForm.vue';
|
||||
import SavingThrowForm from '/imports/client/ui/properties/forms/SavingThrowForm.vue';
|
||||
import SkillForm from '/imports/client/ui/properties/forms/SkillForm.vue';
|
||||
import SlotForm from '/imports/client/ui/properties/forms/SlotForm.vue';
|
||||
import SlotFillerForm from '/imports/client/ui/properties/forms/SlotFillerForm.vue';
|
||||
import SpellListForm from '/imports/client/ui/properties/forms/SpellListForm.vue';
|
||||
import SpellForm from '/imports/client/ui/properties/forms/SpellForm.vue';
|
||||
import ToggleForm from '/imports/client/ui/properties/forms/ToggleForm.vue';
|
||||
import TriggerForm from '/imports/client/ui/properties/forms/TriggerForm.vue';
|
||||
|
||||
export default {
|
||||
action: ActionForm,
|
||||
adjustment: AdjustmentForm,
|
||||
attribute: AttributeForm,
|
||||
buff: BuffForm,
|
||||
buffRemover: BuffRemoverForm,
|
||||
branch: BranchForm,
|
||||
constant: ConstantForm,
|
||||
container: ContainerForm,
|
||||
class: ClassForm,
|
||||
classLevel: ClassLevelForm,
|
||||
damage: DamageForm,
|
||||
damageMultiplier: DamageMultiplierForm,
|
||||
effect: EffectForm,
|
||||
feature: FeatureForm,
|
||||
folder: FolderForm,
|
||||
item: ItemForm,
|
||||
note: NoteForm,
|
||||
pointBuy: PointBuyForm,
|
||||
proficiency: ProficiencyForm,
|
||||
propertySlot: SlotForm,
|
||||
reference: ReferenceForm,
|
||||
roll: RollForm,
|
||||
savingThrow: SavingThrowForm,
|
||||
skill: SkillForm,
|
||||
slotFiller: SlotFillerForm,
|
||||
spellList: SpellListForm,
|
||||
spell: SpellForm,
|
||||
toggle: ToggleForm,
|
||||
trigger: TriggerForm,
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import ComputedField from '/imports/client/ui/properties/forms/shared/ComputedField.vue';
|
||||
import InlineComputationField from '/imports/client/ui/properties/forms/shared/InlineComputationField.vue';
|
||||
import FormSection, { FormSections } from '/imports/client/ui/properties/forms/shared/FormSection.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ComputedField,
|
||||
InlineComputationField,
|
||||
FormSection,
|
||||
FormSections,
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
type: [Object, Array],
|
||||
default: () => ({}),
|
||||
},
|
||||
errors: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
mounted(){
|
||||
// Don't autofocus on mobile, it brings up the on-screen keyboard
|
||||
if (this.$vuetify.breakpoint.smAndDown) return;
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.$refs.focusFirst && this.$refs.focusFirst.focus){
|
||||
this.$refs.focusFirst.focus()
|
||||
}
|
||||
}, 300);
|
||||
},
|
||||
methods: {
|
||||
change(path, value, ack){
|
||||
if (!Array.isArray(path)){
|
||||
path = [path];
|
||||
}
|
||||
this.$emit('change', {path, value, ack});
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Forms that take in a schema and a model of the current data, manages smart
|
||||
* inputs, and sends update events when valid data model changes must occur
|
||||
*/
|
||||
import { get, toPath } from 'lodash';
|
||||
|
||||
function resolvePath(model, path, set) {
|
||||
let arrayPath = toPath(path);
|
||||
if (arrayPath.length === 1) {
|
||||
return { object: model, key: arrayPath[0] };
|
||||
}
|
||||
let key = arrayPath.slice(-1);
|
||||
let objectPath = arrayPath.slice(0, -1);
|
||||
let object = model;
|
||||
// Ensure that nested objects exist before navigating them
|
||||
objectPath.forEach(pathKey => {
|
||||
let newObject = object[pathKey];
|
||||
if (!newObject) {
|
||||
newObject = {};
|
||||
set(object, pathKey, newObject);
|
||||
}
|
||||
object = newObject;
|
||||
});
|
||||
return { object, key };
|
||||
}
|
||||
|
||||
const schemaFormMixin = {
|
||||
data() {
|
||||
return {
|
||||
valid: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
errors() {
|
||||
this.valid = true;
|
||||
if (!this.model) {
|
||||
throw new Error('this.model must be set');
|
||||
}
|
||||
if (!this.validationContext) return {};
|
||||
let cleanModel = this.validationContext.clean(this.model, {
|
||||
getAutoValues: false,
|
||||
});
|
||||
this.validationContext.validate(cleanModel);
|
||||
let errors = {};
|
||||
this.validationContext.validationErrors().forEach(error => {
|
||||
if (this.valid) this.valid = false;
|
||||
errors[error.name] = this.schema.messageForError(error);
|
||||
});
|
||||
return errors;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// Sets the value at the given path
|
||||
change({ path, value, ack }) {
|
||||
let { object, key } = resolvePath(this.model, path, this.$set);
|
||||
|
||||
this.$set(object, key, value);
|
||||
if (ack) ack();
|
||||
},
|
||||
push({ path, value, ack }) {
|
||||
let array = get(this.model, path);
|
||||
if (array === undefined) {
|
||||
let { object, key } = resolvePath(this.model, path, this.$set);
|
||||
this.$set(object, key, [value]);
|
||||
} else if (!array.push) {
|
||||
throw `${path.join('.')} is ${array}, doesn't have "push"`
|
||||
} else {
|
||||
array.push(value);
|
||||
}
|
||||
if (ack) ack();
|
||||
},
|
||||
pull({ path, ack }) {
|
||||
let { object, key } = resolvePath(this.model, path, this.$set);
|
||||
if (!object || !object.splice) {
|
||||
throw `${path.join('.')} is ${object}, doesnt have "splice"`
|
||||
}
|
||||
object.splice(key, 1);
|
||||
if (ack) ack();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default schemaFormMixin;
|
||||
Reference in New Issue
Block a user