PreviewDoor combining SVGRect and foreignObject

This commit is contained in:
Jörn-Michael Miehe 2023-02-17 19:00:27 +00:00
parent bb89d49f0d
commit 2551b0480b
3 changed files with 151 additions and 138 deletions

View file

@ -3,28 +3,21 @@
<div class="content">
<p class="title is-5">Steuerung</p>
<ul>
<li>Linksklick: Türchen auswählen</li>
<li>Tastatur [0]-[9], [Rücktaste]: Tag eingeben</li>
<li>Tastatur [Enter]: Tag speichern</li>
<li>Tastatur [Esc]: Eingabe Abbrechen</li>
<li>Tastatur [Entf]: Tag entfernen</li>
<li>Linksklick: Türchen bearbeiten</li>
<li>Tastatur: Tag eingeben</li>
<li>[Enter]: Tag speichern</li>
<li>[Esc]: Eingabe Abbrechen</li>
<li>[Entf]: Tag entfernen</li>
</ul>
</div>
<figure class="image">
<img src="@/assets/adventskalender.jpg" />
<ThouCanvas>
<template v-for="(door, index) in doors" :key="`door-${index}`">
<SVGRectText
v-if="door.day >= 0"
:text="String(door.day)"
:rectangle="door.position"
/>
<SVGRect
:rectangle="door.position"
:focused="index === focused"
@click.left="click_door(index)"
/>
</template>
<PreviewDoor
v-for="(_, index) in doors"
:key="`door-${index}`"
v-model:door="doors[index]"
/>
</ThouCanvas>
</figure>
</section>
@ -35,14 +28,12 @@ import { Options, Vue } from "vue-class-component";
import { Door } from "./calendar";
import ThouCanvas from "../rects/ThouCanvas.vue";
import SVGRect from "../rects/SVGRect.vue";
import SVGRectText from "../rects/SVGRectText.vue";
import PreviewDoor from "./PreviewDoor.vue";
@Options({
components: {
ThouCanvas,
SVGRect,
SVGRectText,
PreviewDoor,
},
props: {
doors: Array,
@ -51,53 +42,6 @@ import SVGRectText from "../rects/SVGRectText.vue";
})
export default class extends Vue {
private doors!: Door[];
private focused = -1;
private click_door(index: number) {
if (this.focused >= 0) {
return;
}
this.focused = index;
const listener = (() => {
let old_day = this.doors[index].day;
let num_input = "";
return (event: KeyboardEvent) => {
if (event.key >= "0" && event.key <= "9") {
// number input
num_input += event.key;
this.doors[this.focused].day = Number(num_input);
} else if (event.key === "Backspace") {
// remove char
num_input = num_input.slice(0, -1);
this.doors[this.focused].day = Number(num_input);
} else if (event.key === "Enter") {
// accept
this.focused = -1;
window.removeEventListener("keydown", listener);
} else if (event.key === "Escape") {
// abort
this.doors[this.focused].day = old_day;
this.focused = -1;
window.removeEventListener("keydown", listener);
} else if (event.key === "Delete") {
// delete
this.doors[this.focused].day = -1;
this.focused = -1;
window.removeEventListener("keydown", listener);
}
};
})();
window.addEventListener("keydown", listener);
}
public beforeUnmount() {
this.$emit("update:doors", this.doors);
@ -108,9 +52,5 @@ export default class extends Vue {
<style lang="scss" scoped>
section > figure {
user-select: none;
svg > rect {
cursor: pointer;
}
}
</style>

View file

@ -0,0 +1,139 @@
<template>
<SVGRect :rectangle="door.position" :focused="editing" />
<foreignObject
:x="Math.round(parent_aspect_ratio * door.position.left)"
:y="door.position.top"
:width="Math.round(parent_aspect_ratio * door.position.width)"
:height="door.position.height"
:style="`transform: scaleX(${1 / parent_aspect_ratio})`"
>
<div
xmlns="http://www.w3.org/1999/xhtml"
class="is-flex is-align-items-center"
@click.left="on_click"
>
<div class="container is-fluid has-text-centered">
<input
v-if="editing"
v-model="day_str"
@keyup="on_keyup"
class="input p-3 is-size-2"
type="number"
min="-1"
placeholder="Tag"
/>
<div v-else class="is-size-1 has-text-weight-bold">
<template v-if="door.day >= 0">{{ door.day }}</template>
</div>
</div>
</div>
</foreignObject>
</template>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { Door } from "./calendar";
import SVGRect from "../rects/SVGRect.vue";
@Options({
components: {
SVGRect,
},
props: {
door: Door,
editable: {
type: Boolean,
default: true,
},
},
emits: ["update:door"],
})
export default class extends Vue {
private door!: Door;
private editable!: boolean;
private day_str = "";
private editing = false;
private refreshKey = 0;
private refresh() {
window.setTimeout(() => {
// don't loop endlessly
if (this.refreshKey < 10000) {
this.refreshKey++;
}
}, 100);
}
private get parent_aspect_ratio(): number {
this.refreshKey; // read it just to force recompute on change
if (!(this.$el instanceof Text) || this.$el.parentElement === null) {
this.refresh();
return 1;
}
const parent = this.$el.parentElement;
const result = parent.clientWidth / parent.clientHeight;
// force recompute for suspicious results
if (result === 0 || result === Infinity) {
this.refresh();
return 1;
}
return result;
}
private toggle_editing() {
this.day_str = String(this.door.day);
this.editing = this.editable && !this.editing;
}
private on_click(event: MouseEvent) {
if (event.target === null) {
return;
}
if (this.editing) {
this.door.day = Number(this.day_str);
}
if (event.target instanceof HTMLDivElement) {
this.toggle_editing();
}
}
private on_keyup(event: KeyboardEvent) {
if (!this.editing) {
return;
}
if (event.key === "Enter") {
this.door.day = Number(this.day_str);
this.toggle_editing();
} else if (event.key === "Delete") {
this.door.day = -1;
this.toggle_editing();
} else if (event.key === "Escape") {
this.toggle_editing();
}
}
public beforeUnmount() {
this.$emit("update:door", this.door);
}
}
</script>
<style lang="scss" scoped>
foreignObject {
cursor: pointer;
> div {
color: red;
height: inherit;
}
}
</style>

View file

@ -1,66 +0,0 @@
<template>
<foreignObject
:x="Math.round(parent_aspect_ratio * rectangle.left)"
:y="rectangle.top"
:width="Math.round(parent_aspect_ratio * rectangle.width)"
:height="rectangle.height"
:style="`transform: scaleX(${1 / parent_aspect_ratio})`"
>
<div
xmlns="http://www.w3.org/1999/xhtml"
class="is-flex is-align-items-center is-justify-content-center"
>
<div class="is-size-1 has-text-weight-bold">{{ text }}</div>
</div>
</foreignObject>
</template>
<script lang="ts">
import { Vue, Options } from "vue-class-component";
import { Rectangle } from "./rectangles";
@Options({
props: {
rectangle: Rectangle,
text: String,
},
})
export default class extends Vue {
private rectangle!: Rectangle;
private text!: string;
private refreshKey = 0;
private get parent_aspect_ratio(): number {
this.refreshKey; // read it just to force recompute on change
if (!(this.$el instanceof Element) || this.$el.parentElement === null) {
return 1;
}
const parent = this.$el.parentElement;
const result = parent.clientWidth / parent.clientHeight;
// force recompute for suspicious results
if (result === 0 || result === Infinity) {
// don't loop endlessly
if (this.refreshKey < 10000) {
this.refreshKey++;
}
return 1;
}
return result;
}
public mounted() {
this.refreshKey++;
}
}
</script>
<style lang="scss" scoped>
foreignObject > div {
color: red;
height: inherit;
}
</style>