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 }" />
|
<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) {
|
||||||
|
|
77
src/app.css
77
src/app.css
|
@ -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
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" 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)"> </button>
|
<button class="settings" @click="editObject(o)"> </button>
|
||||||
<button class="delete" @click="deleteObject(o)"> </button>
|
<button class="delete" @click="deleteObject(o)"> </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>
|
||||||
|
|
|
@ -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')"> </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"> </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>
|
</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>
|
||||||
|
|
Loading…
Add table
Reference in a new issue