fully interactive object manipulation, delete/restore
This commit is contained in:
parent
6d016f68fe
commit
38f86896f9
6 changed files with 151 additions and 80 deletions
47
src/App.vue
47
src/App.vue
|
@ -7,7 +7,17 @@
|
|||
<SystemDiagram v-bind="{ star, objects }" />
|
||||
|
||||
<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>
|
||||
<li>Edit planets by clicking directly inside the graphic or in the table below.</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>
|
||||
</Tips>
|
||||
<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>
|
||||
|
||||
</template>
|
||||
|
@ -41,13 +51,38 @@ const labelFonts = ['xolonium', 'douar', 'lack']
|
|||
const themes = ['default', 'retro', 'inverse', 'paper']
|
||||
|
||||
const selectedObject = ref(null)
|
||||
const deletedObject = ref(null) // { index: Number, object: Object }
|
||||
|
||||
function editObject (obj) {
|
||||
selectedObject.value = obj
|
||||
function editObject (object) {
|
||||
selectedObject.value = object
|
||||
}
|
||||
|
||||
function deleteObject (obj) {
|
||||
console.log('delete object not yet implemented')
|
||||
function deleteObject (object) {
|
||||
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) {
|
||||
|
|
77
src/app.css
77
src/app.css
|
@ -29,6 +29,7 @@
|
|||
--bg-settings: var(--bg-graphic);
|
||||
--fg-settings: var(--fg-graphic);
|
||||
--title-font: xolonium;
|
||||
--fg-danger: #C00;
|
||||
}
|
||||
|
||||
html,body {
|
||||
|
@ -56,7 +57,7 @@ input[type="text"]:focus {
|
|||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
button.settings, button.delete, button.add, button.less {
|
||||
button.settings, button.delete, button.add, button.less, button.close {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
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.add { background-image: url(./assets/add.png); }
|
||||
button.less { background-image: url(./assets/less.png); }
|
||||
button.close { background-image: url(./assets/close.png); }
|
||||
|
||||
.tip {
|
||||
width: calc(100% - 4em);
|
||||
|
@ -116,35 +118,6 @@ body.title-douar { --title-font: 'douar'; }
|
|||
body.title-lack { --title-font: 'lack'; }
|
||||
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); }
|
||||
line { stroke: var(--fg-graphic); }
|
||||
text.tilted { transform: rotate(-45deg) translate(0, 100%); transform-origin: left top; transform-box: fill-box; }
|
||||
|
@ -211,6 +184,7 @@ h1 {
|
|||
}
|
||||
|
||||
#object-list {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 3em auto;
|
||||
|
@ -226,35 +200,32 @@ h1 {
|
|||
padding: .25em 1em;
|
||||
}
|
||||
|
||||
.object-settings {
|
||||
#object-settings {
|
||||
margin-bottom: 5em;
|
||||
}
|
||||
.object-settings > header {
|
||||
#object-settings section.main {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.object-settings > header > p {
|
||||
margin-left: 2em;
|
||||
}
|
||||
.object-settings > header input[type="text"] {
|
||||
#object-settings input[type="text"].big {
|
||||
width: 7em;
|
||||
padding: .1em .5em;
|
||||
font-size: 1em;
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
.object-settings input[type="range"] { width: 100%; }
|
||||
.object-settings input[type="number"] {
|
||||
#object-settings input[type="range"] { width: 100%; }
|
||||
#object-settings input[type="number"] {
|
||||
width: 4em;
|
||||
padding: .2em 0;
|
||||
text-align: center;
|
||||
}
|
||||
.object-settings .satellite-list {
|
||||
|
||||
#object-settings section.satellite-list {
|
||||
margin: 2em 0;
|
||||
}
|
||||
.object-settings .satellite {
|
||||
.cta {
|
||||
vertical-align: middle;
|
||||
margin: 0 .2em;
|
||||
margin: .2em;
|
||||
padding: 2px .5em;
|
||||
font-size: 1.2em;
|
||||
background: var(--bg-app);
|
||||
|
@ -263,7 +234,23 @@ h1 {
|
|||
border-radius: 8px;
|
||||
transition: border-color .2s ease-out;
|
||||
}
|
||||
.object-settings .satellite:hover {
|
||||
.cta:hover {
|
||||
outline: 1px solid var(--fg-app);
|
||||
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
BIN
src/assets/close.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 961 B |
BIN
src/assets/close_inverted.png
Normal file
BIN
src/assets/close_inverted.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 897 B |
|
@ -4,7 +4,7 @@
|
|||
<th scope="col" v-for="col in columns">{{ col }}</th>
|
||||
<th scope="col">actions</th>
|
||||
</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">
|
||||
<div class="cell">{{ o[value] }}</div>
|
||||
</td>
|
||||
|
@ -13,17 +13,35 @@
|
|||
<button class="settings" @click="editObject(o)"> </button>
|
||||
<button class="delete" @click="deleteObject(o)"> </button>
|
||||
</div></td>
|
||||
<button v-if="i === deletedObject?.index"
|
||||
class="deleted-overlay"
|
||||
@click="restoreDeleted"
|
||||
>
|
||||
RESTORE DELETED OBJECT
|
||||
</button>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
objects: Array,
|
||||
deletedObject: [Object, null],
|
||||
editObject: Function,
|
||||
deleteObject: Function,
|
||||
restoreDeleted: Function,
|
||||
})
|
||||
|
||||
const columns = ['Δ', 'Name', 'Type', 'Radius', 'Rings', 'Satellites']
|
||||
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>
|
||||
|
|
|
@ -1,28 +1,47 @@
|
|||
<template>
|
||||
<div class="object-settings">
|
||||
<header>
|
||||
<h2><input type="text" v-model="name" @blur="checkName"/></h2>
|
||||
<p>
|
||||
<div id="object-settings">
|
||||
<section class="main">
|
||||
<div>
|
||||
<input type="text" class="big"
|
||||
:value="name"
|
||||
@input="checkName($event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
Distance Δ:
|
||||
<input type="number" min="50" max="1000" v-model="distance" />
|
||||
</p>
|
||||
<p>
|
||||
<input type="number" min="50" max="1000"
|
||||
:value="distance"
|
||||
@input="update('distance', $event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
Radius r:
|
||||
<input type="number" min="1" max="125" v-model="radius" />
|
||||
</p>
|
||||
<p>
|
||||
<input type="number" min="1" max="125"
|
||||
:value="radius"
|
||||
@input="update('radius', $event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
Rings:
|
||||
<input type="number" min="0" max="15" v-model="rings" />
|
||||
</p>
|
||||
</header>
|
||||
<div class="satellite-list">
|
||||
<input type="number" min="0" max="15"
|
||||
:value="rings"
|
||||
@input="update('rings', $event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
<button class="close" title="close" @click="$emit('close')"> </button>
|
||||
</section>
|
||||
<section class="satellite-list">
|
||||
Satellites:
|
||||
<button class="satellite" v-for="satellite in satellites">
|
||||
<button class="cta" v-for="satellite in satellites">
|
||||
{{ satellite.name }}
|
||||
<template v-if="satellite.type">({{ satellite.type }})</template>
|
||||
</button>
|
||||
<button class="add"> </button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="additional-options">
|
||||
Other options:
|
||||
<button class="cta danger" @click="$emit('delete')">REMOVE OBJECT</button>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -30,21 +49,25 @@
|
|||
import { ref, computed } from 'vue'
|
||||
|
||||
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([
|
||||
'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(() => {
|
||||
if (!satellites.value || !satellites.value.length) return 'none'
|
||||
return satellites.value.reduce((acc, satellite) => {
|
||||
|
@ -55,8 +78,16 @@ const satellitesList = computed(() => {
|
|||
}, []).join(', ')
|
||||
})
|
||||
|
||||
const numberTargets = ['distance', 'radius', 'rings']
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
function checkName (name) {
|
||||
if (!name.trim().length) name = props.autoName
|
||||
update('name', name)
|
||||
}
|
||||
</script>
|
||||
|
|
Loading…
Add table
Reference in a new issue