fully interactive object manipulation, delete/restore

This commit is contained in:
koehr 2022-01-04 14:10:01 +01:00
parent 6d016f68fe
commit 38f86896f9
6 changed files with 151 additions and 80 deletions

View file

@ -7,7 +7,17 @@
<SystemDiagram v-bind="{ star, objects }" /> <SystemDiagram v-bind="{ star, objects }" />
<section id="settings"> <section id="settings">
<ObjectSettings v-model:object="selectedObject" v-if="selectedObject" /> <ObjectSettings v-if="selectedObject"
v-model:name="selectedObject.name"
v-model:distance="selectedObject.distance"
v-model:type="selectedObject.type"
v-model:radius="selectedObject.radius"
v-model:rings="selectedObject.rings"
v-model:satellites="selectedObject.satellites"
:auto-name="autoName(selectedObject)"
@delete="deleteObject"
@close="editObject(null)"
/>
<Tips> <Tips>
<li>Edit planets by clicking directly inside the graphic or in the table below.</li> <li>Edit planets by clicking directly inside the graphic or in the table below.</li>
<li>Drag planets around to change their distance.</li> <li>Drag planets around to change their distance.</li>
@ -16,7 +26,7 @@
<li>You can also drag satellite buttons around to reorder them.</li> <li>You can also drag satellite buttons around to reorder them.</li>
</Tips> </Tips>
<SystemSettings v-model:designation="star.designation" v-model:radius="star.radius" /> <SystemSettings v-model:designation="star.designation" v-model:radius="star.radius" />
<ObjectList v-bind="{ objects, editObject, deleteObject }" /> <ObjectList v-bind="{ objects, deletedObject, editObject, deleteObject, restoreDeleted }" />
</section> </section>
</template> </template>
@ -41,13 +51,38 @@ const labelFonts = ['xolonium', 'douar', 'lack']
const themes = ['default', 'retro', 'inverse', 'paper'] const themes = ['default', 'retro', 'inverse', 'paper']
const selectedObject = ref(null) const selectedObject = ref(null)
const deletedObject = ref(null) // { index: Number, object: Object }
function editObject (obj) { function editObject (object) {
selectedObject.value = obj selectedObject.value = object
} }
function deleteObject (obj) { function deleteObject (object) {
console.log('delete object not yet implemented') if (deletedObject.value) {
const lost = deletedObject.value.object.name
const confirmed = confirm(`
Attention! Only the LAST deleted object can be restored.
${lost} will be lost forever! Proceed anyway?`
)
if (!confirmed) return
}
if (!object) object = selectedObject.value
const index = objects.value.indexOf(object)
console.debug('deleting object at index', index)
if (index >= 0) objects.value.splice(index, 1)
if (object === selectedObject.value) selectedObject.value = null
deletedObject.value = { index, object }
}
function restoreDeleted () {
const { index, object } = deletedObject.value
console.debug('restoring deleted object', index)
objects.value.splice(index, 0, object)
deletedObject.value = null
} }
function autoName (obj) { function autoName (obj) {

View file

@ -29,6 +29,7 @@
--bg-settings: var(--bg-graphic); --bg-settings: var(--bg-graphic);
--fg-settings: var(--fg-graphic); --fg-settings: var(--fg-graphic);
--title-font: xolonium; --title-font: xolonium;
--fg-danger: #C00;
} }
html,body { html,body {
@ -56,7 +57,7 @@ input[type="text"]:focus {
button { button {
cursor: pointer; cursor: pointer;
} }
button.settings, button.delete, button.add, button.less { button.settings, button.delete, button.add, button.less, button.close {
width: 48px; width: 48px;
height: 48px; height: 48px;
border: 4px solid var(--bg-app); border: 4px solid var(--bg-app);
@ -70,6 +71,7 @@ button.settings { background-image: url(./assets/change.png); }
button.delete { background-image: url(./assets/delete.png); } button.delete { background-image: url(./assets/delete.png); }
button.add { background-image: url(./assets/add.png); } button.add { background-image: url(./assets/add.png); }
button.less { background-image: url(./assets/less.png); } button.less { background-image: url(./assets/less.png); }
button.close { background-image: url(./assets/close.png); }
.tip { .tip {
width: calc(100% - 4em); width: calc(100% - 4em);
@ -116,35 +118,6 @@ body.title-douar { --title-font: 'douar'; }
body.title-lack { --title-font: 'lack'; } body.title-lack { --title-font: 'lack'; }
body.title-xolonium { --title-font: 'xolonium'; } body.title-xolonium { --title-font: 'xolonium'; }
.menu-item {
padding: 1em;
}
.menu-item > label {
cursor: pointer;
}
.menu-item > label::before {
content: "▸ ";
}
.menu-item.open > label::before {
content: "▾ ";
}
.menu-item.open {
background: var(--bg-settings);
color: var(--fg-settings);
}
.menu-item > .object-settings {
display: none;
position: absolute;
left: 0;
width: calc(100vw - 4em);
margin-top: 1em;
padding: 1em 2em;
background: var(--bg-graphic);
}
.menu-item.open > .object-settings {
display: block;
}
svg { background: var(--bg-graphic); } svg { background: var(--bg-graphic); }
line { stroke: var(--fg-graphic); } line { stroke: var(--fg-graphic); }
text.tilted { transform: rotate(-45deg) translate(0, 100%); transform-origin: left top; transform-box: fill-box; } text.tilted { transform: rotate(-45deg) translate(0, 100%); transform-origin: left top; transform-box: fill-box; }
@ -211,6 +184,7 @@ h1 {
} }
#object-list { #object-list {
position: relative;
display: block; display: block;
width: 100%; width: 100%;
margin: 3em auto; margin: 3em auto;
@ -226,35 +200,32 @@ h1 {
padding: .25em 1em; padding: .25em 1em;
} }
.object-settings { #object-settings {
margin-bottom: 5em; margin-bottom: 5em;
} }
.object-settings > header { #object-settings section.main {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 1em;
} }
.object-settings > header > p { #object-settings input[type="text"].big {
margin-left: 2em;
}
.object-settings > header input[type="text"] {
width: 7em; width: 7em;
padding: .1em .5em; padding: .1em .5em;
font-size: 1em; font-size: 1.5em;
font-weight: bold; font-weight: bold;
} }
.object-settings input[type="range"] { width: 100%; } #object-settings input[type="range"] { width: 100%; }
.object-settings input[type="number"] { #object-settings input[type="number"] {
width: 4em; width: 4em;
padding: .2em 0; padding: .2em 0;
text-align: center; text-align: center;
} }
.object-settings .satellite-list { #object-settings section.satellite-list {
margin: 2em 0;
} }
.object-settings .satellite { .cta {
vertical-align: middle; vertical-align: middle;
margin: 0 .2em; margin: .2em;
padding: 2px .5em; padding: 2px .5em;
font-size: 1.2em; font-size: 1.2em;
background: var(--bg-app); background: var(--bg-app);
@ -263,7 +234,23 @@ h1 {
border-radius: 8px; border-radius: 8px;
transition: border-color .2s ease-out; transition: border-color .2s ease-out;
} }
.object-settings .satellite:hover { .cta:hover {
outline: 1px solid var(--fg-app); outline: 1px solid var(--fg-app);
border-color: transparent; border-color: transparent;
} }
.danger {
color: var(--fg-danger);
font-weight: bold;
}
.deleted-overlay {
position: absolute;
left: 0;
display: block;
background: #0008;
border: none;
border-radius: 8px;
width: 100%;
height: 4.4em;
color: var(--fg-app);
font-weight: bold;
}

BIN
src/assets/close.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 961 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 897 B

View file

@ -4,7 +4,7 @@
<th scope="col" v-for="col in columns">{{ col }}</th> <th scope="col" v-for="col in columns">{{ col }}</th>
<th scope="col">actions</th> <th scope="col">actions</th>
</tr> </tr>
<tr v-for="o in objects"> <tr :class="{ deleted: i === deletedObject?.index }" v-for="o,i in objectList">
<td v-for="value in values"> <td v-for="value in values">
<div class="cell">{{ o[value] }}</div> <div class="cell">{{ o[value] }}</div>
</td> </td>
@ -13,17 +13,35 @@
<button class="settings" @click="editObject(o)">&nbsp;</button> <button class="settings" @click="editObject(o)">&nbsp;</button>
<button class="delete" @click="deleteObject(o)">&nbsp;</button> <button class="delete" @click="deleteObject(o)">&nbsp;</button>
</div></td> </div></td>
<button v-if="i === deletedObject?.index"
class="deleted-overlay"
@click="restoreDeleted"
>
RESTORE DELETED OBJECT
</button>
</tr> </tr>
</table> </table>
</template> </template>
<script setup> <script setup>
import { computed } from 'vue'
const props = defineProps({ const props = defineProps({
objects: Array, objects: Array,
deletedObject: [Object, null],
editObject: Function, editObject: Function,
deleteObject: Function, deleteObject: Function,
restoreDeleted: Function,
}) })
const columns = ['Δ', 'Name', 'Type', 'Radius', 'Rings', 'Satellites'] const columns = ['Δ', 'Name', 'Type', 'Radius', 'Rings', 'Satellites']
const values = ['distance', 'name', 'type', 'radius', 'rings'] const values = ['distance', 'name', 'type', 'radius', 'rings']
const objectList = computed(() => {
if (!props.deletedObject) return props.objects
const { index, object } = props.deletedObject
const objects = [ ...props.objects ]
objects.splice(index, 0, object)
return objects
})
</script> </script>

View file

@ -1,28 +1,47 @@
<template> <template>
<div class="object-settings"> <div id="object-settings">
<header> <section class="main">
<h2><input type="text" v-model="name" @blur="checkName"/></h2> <div>
<p> <input type="text" class="big"
:value="name"
@input="checkName($event.target.value)"
/>
</div>
<div>
Distance Δ: Distance Δ:
<input type="number" min="50" max="1000" v-model="distance" /> <input type="number" min="50" max="1000"
</p> :value="distance"
<p> @input="update('distance', $event.target.value)"
/>
</div>
<div>
Radius r: Radius r:
<input type="number" min="1" max="125" v-model="radius" /> <input type="number" min="1" max="125"
</p> :value="radius"
<p> @input="update('radius', $event.target.value)"
/>
</div>
<div>
Rings: Rings:
<input type="number" min="0" max="15" v-model="rings" /> <input type="number" min="0" max="15"
</p> :value="rings"
</header> @input="update('rings', $event.target.value)"
<div class="satellite-list"> />
</div>
<button class="close" title="close" @click="$emit('close')">&nbsp;</button>
</section>
<section class="satellite-list">
Satellites: Satellites:
<button class="satellite" v-for="satellite in satellites"> <button class="cta" v-for="satellite in satellites">
{{ satellite.name }} {{ satellite.name }}
<template v-if="satellite.type">({{ satellite.type }})</template> <template v-if="satellite.type">({{ satellite.type }})</template>
</button> </button>
<button class="add">&nbsp;</button> <button class="add">&nbsp;</button>
</div> </section>
<section class="additional-options">
Other options:
<button class="cta danger" @click="$emit('delete')">REMOVE OBJECT</button>
</section>
</div> </div>
</template> </template>
@ -30,21 +49,25 @@
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
const props = defineProps({ const props = defineProps({
object: Object, distance: Number,
name: String,
type: String,
radius: Number,
rings: Number,
satellites: Array,
autoName: String, // auto generated name, like Sol-3
}) })
const emit = defineEmits([ const emit = defineEmits([
'update:object', 'update:distance',
'update:name',
'update:type',
'update:radius',
'update:rings',
'update:satellites',
'delete',
'close',
]) ])
const tipsShown = ref(true)
const distance = ref(props.object.distance)
const name = ref(props.object.name)
const type = ref(props.object.type)
const radius = ref(props.object.radius)
const rings = ref(props.object.rings)
const satellites = ref(props.object.satellites)
const satellitesList = computed(() => { const satellitesList = computed(() => {
if (!satellites.value || !satellites.value.length) return 'none' if (!satellites.value || !satellites.value.length) return 'none'
return satellites.value.reduce((acc, satellite) => { return satellites.value.reduce((acc, satellite) => {
@ -55,8 +78,16 @@ const satellitesList = computed(() => {
}, []).join(', ') }, []).join(', ')
}) })
const numberTargets = ['distance', 'radius', 'rings']
function update (target, value) { function update (target, value) {
if (target === 'radius') value = parseInt(value) console.debug('updating', target, 'with', value)
if (numberTargets.indexOf(target) >= 0) value = parseInt(value)
emit(`update:${target}`, value) emit(`update:${target}`, value)
} }
function checkName (name) {
if (!name.trim().length) name = props.autoName
update('name', name)
}
</script> </script>