PreviewDoor combining SVGRect and foreignObject
This commit is contained in:
parent
bb89d49f0d
commit
2551b0480b
3 changed files with 151 additions and 138 deletions
|
@ -3,28 +3,21 @@
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<p class="title is-5">Steuerung</p>
|
<p class="title is-5">Steuerung</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Linksklick: Türchen auswählen</li>
|
<li>Linksklick: Türchen bearbeiten</li>
|
||||||
<li>Tastatur [0]-[9], [Rücktaste]: Tag eingeben</li>
|
<li>Tastatur: Tag eingeben</li>
|
||||||
<li>Tastatur [Enter]: Tag speichern</li>
|
<li>[Enter]: Tag speichern</li>
|
||||||
<li>Tastatur [Esc]: Eingabe Abbrechen</li>
|
<li>[Esc]: Eingabe Abbrechen</li>
|
||||||
<li>Tastatur [Entf]: Tag entfernen</li>
|
<li>[Entf]: Tag entfernen</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<figure class="image">
|
<figure class="image">
|
||||||
<img src="@/assets/adventskalender.jpg" />
|
<img src="@/assets/adventskalender.jpg" />
|
||||||
<ThouCanvas>
|
<ThouCanvas>
|
||||||
<template v-for="(door, index) in doors" :key="`door-${index}`">
|
<PreviewDoor
|
||||||
<SVGRectText
|
v-for="(_, index) in doors"
|
||||||
v-if="door.day >= 0"
|
:key="`door-${index}`"
|
||||||
:text="String(door.day)"
|
v-model:door="doors[index]"
|
||||||
:rectangle="door.position"
|
/>
|
||||||
/>
|
|
||||||
<SVGRect
|
|
||||||
:rectangle="door.position"
|
|
||||||
:focused="index === focused"
|
|
||||||
@click.left="click_door(index)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</ThouCanvas>
|
</ThouCanvas>
|
||||||
</figure>
|
</figure>
|
||||||
</section>
|
</section>
|
||||||
|
@ -35,14 +28,12 @@ import { Options, Vue } from "vue-class-component";
|
||||||
import { Door } from "./calendar";
|
import { Door } from "./calendar";
|
||||||
|
|
||||||
import ThouCanvas from "../rects/ThouCanvas.vue";
|
import ThouCanvas from "../rects/ThouCanvas.vue";
|
||||||
import SVGRect from "../rects/SVGRect.vue";
|
import PreviewDoor from "./PreviewDoor.vue";
|
||||||
import SVGRectText from "../rects/SVGRectText.vue";
|
|
||||||
|
|
||||||
@Options({
|
@Options({
|
||||||
components: {
|
components: {
|
||||||
ThouCanvas,
|
ThouCanvas,
|
||||||
SVGRect,
|
PreviewDoor,
|
||||||
SVGRectText,
|
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
doors: Array,
|
doors: Array,
|
||||||
|
@ -51,53 +42,6 @@ import SVGRectText from "../rects/SVGRectText.vue";
|
||||||
})
|
})
|
||||||
export default class extends Vue {
|
export default class extends Vue {
|
||||||
private doors!: Door[];
|
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() {
|
public beforeUnmount() {
|
||||||
this.$emit("update:doors", this.doors);
|
this.$emit("update:doors", this.doors);
|
||||||
|
@ -108,9 +52,5 @@ export default class extends Vue {
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
section > figure {
|
section > figure {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
svg > rect {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
139
ui/src/components/door_map/PreviewDoor.vue
Normal file
139
ui/src/components/door_map/PreviewDoor.vue
Normal 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>
|
|
@ -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>
|
|
Loading…
Reference in a new issue