🔧 ui: overall typescript compliance

- reduce use of `as` in favor of Vue's `UnwrapRef`
- add `wait_for` helper function

⚠️ known bug: PreviewDoor (DoorChooser) onClick not working
This commit is contained in:
Jörn-Michael Miehe 2025-12-25 19:56:25 +00:00
parent 6ff5af45d5
commit fd1a66ba25
12 changed files with 70 additions and 55 deletions

View file

@ -49,9 +49,9 @@
<script setup lang="ts">
import { API } from "@/lib/api";
import { APIError } from "@/lib/api_error";
import { ensure_loaded, name_door } from "@/lib/helpers";
import { ensure_loaded, Like, name_door } from "@/lib/helpers";
import { ImageData } from "@/lib/model";
import { VueDoor } from "@/lib/rects/door";
import { Door } from "@/lib/rects/door";
import { advent22Store } from "@/lib/store";
import { onBeforeUnmount } from "vue";
@ -62,7 +62,7 @@ import CalendarDoor from "./calendar/CalendarDoor.vue";
import ThouCanvas from "./calendar/ThouCanvas.vue";
defineProps<{
doors: VueDoor[];
doors: Like<Door>[];
}>();
const store = advent22Store();

View file

@ -48,8 +48,9 @@
</template>
<script setup lang="ts">
import { wait_for } from "@/lib/helpers";
import { Credentials } from "@/lib/model";
import { nextTick, onBeforeUnmount, onMounted, ref, useTemplateRef } from "vue";
import { onBeforeUnmount, onMounted, ref, useTemplateRef } from "vue";
import BulmaButton from "./bulma/Button.vue";
const username_input = useTemplateRef("username_input");
@ -78,9 +79,10 @@ onMounted(() => {
window.addEventListener("keydown", on_keydown);
nextTick(() => {
username_input.value?.focus();
});
wait_for(
() => username_input.value !== null,
() => username_input.value!.focus(),
);
onBeforeUnmount(() => {
window.removeEventListener("keydown", on_keydown);

View file

@ -37,10 +37,10 @@
</div>
</div>
<DoorPlacer v-if="current_step === 0" v-model="(doors as Door[])" />
<DoorChooser v-if="current_step === 1" v-model="(doors as Door[])" />
<DoorPlacer v-if="current_step === 0" v-model="doors" />
<DoorChooser v-if="current_step === 1" v-model="doors" />
<div v-if="current_step === 2" class="card-content">
<Calendar :doors="(doors as Door[])" />
<Calendar :doors="doors" />
</div>
<footer class="card-footer is-flex is-justify-content-space-around">

View file

@ -14,16 +14,17 @@
</template>
<script setup lang="ts">
import { VueDoor } from "@/lib/rects/door";
import { Door } from "@/lib/rects/door";
import { advent22Store } from "@/lib/store";
import { Like } from "@/lib/helpers";
import SVGRect from "./SVGRect.vue";
const store = advent22Store();
withDefaults(
defineProps<{
door: VueDoor;
door: Like<Door>;
force_visible?: boolean;
}>(),
{

View file

@ -20,8 +20,8 @@
</template>
<script setup lang="ts">
import { loading_success } from "@/lib/helpers";
import { VueRectangle } from "@/lib/rects/rectangle";
import { Like, loading_success } from "@/lib/helpers";
import { Rectangle } from "@/lib/rects/rectangle";
import { advent22Store } from "@/lib/store";
import { computed } from "vue";
@ -39,7 +39,7 @@ withDefaults(
defineProps<{
variant: BulmaVariant;
visible?: boolean;
rectangle: VueRectangle;
rectangle: Like<Rectangle>;
}>(),
{
visible: true,

View file

@ -29,6 +29,7 @@ import { Rectangle } from "@/lib/rects/rectangle";
import { Vector2D } from "@/lib/rects/vector2d";
import { computed, ref } from "vue";
import { Like } from "@/lib/helpers";
import CalendarDoor from "../calendar/CalendarDoor.vue";
import SVGRect from "../calendar/SVGRect.vue";
import ThouCanvas from "../calendar/ThouCanvas.vue";
@ -36,9 +37,9 @@ import ThouCanvas from "../calendar/ThouCanvas.vue";
type CanvasState =
| { kind: "idle" }
| { kind: "drawing" }
| { kind: "dragging"; door: Door; origin: Vector2D };
| { kind: "dragging"; door: Like<Door>; origin: Vector2D };
const model = defineModel<Door[]>({ required: true });
const model = defineModel<Like<Door>[]>({ required: true });
const MIN_RECT_AREA = 300;
const state = ref<CanvasState>({ kind: "idle" });
@ -46,7 +47,7 @@ const preview = ref(new Rectangle());
const preview_visible = computed(() => state.value.kind !== "idle");
function pop_door(point: Vector2D): Door | undefined {
function pop_door(point: Vector2D): Like<Door> | undefined {
const idx = model.value.findIndex((rect) => rect.position.contains(point));
if (idx === -1) {
@ -71,7 +72,7 @@ function draw_finish() {
}
if (preview.value.area >= MIN_RECT_AREA) {
model.value.push(new Door(preview.value as Rectangle));
model.value.push(new Door(preview.value));
}
state.value = { kind: "idle" };
@ -98,7 +99,7 @@ function drag_finish() {
return;
}
model.value.push(new Door(preview.value as Rectangle, state.value.door.day));
model.value.push(new Door(preview.value, state.value.door.day));
state.value = { kind: "idle" };
}

View file

@ -24,13 +24,13 @@
</template>
<script setup lang="ts">
import { ensure_loaded } from "@/lib/helpers";
import { ensure_loaded, Like } from "@/lib/helpers";
import { Door } from "@/lib/rects/door";
import { advent22Store } from "@/lib/store";
import ThouCanvas from "../calendar/ThouCanvas.vue";
import PreviewDoor from "./PreviewDoor.vue";
const model = defineModel<Door[]>({ required: true });
const model = defineModel<Like<Door>[]>({ required: true });
const store = advent22Store();
</script>

View file

@ -16,12 +16,12 @@
</template>
<script setup lang="ts">
import { ensure_loaded } from "@/lib/helpers";
import { ensure_loaded, Like } from "@/lib/helpers";
import { Door } from "@/lib/rects/door";
import { advent22Store } from "@/lib/store";
import DoorCanvas from "./DoorCanvas.vue";
const model = defineModel<Door[]>({ required: true });
const model = defineModel<Like<Door>[]>({ required: true });
const store = advent22Store();
</script>

View file

@ -24,11 +24,12 @@
<script setup lang="ts">
import { Door } from "@/lib/rects/door";
import { nextTick, ref, useTemplateRef } from "vue";
import { ref, useTemplateRef } from "vue";
import { Like, unwrap_like, wait_for } from "@/lib/helpers";
import SVGRect from "../calendar/SVGRect.vue";
const model = defineModel<Door>({ required: true });
const model = defineModel<Like<Door>>({ required: true });
const day_input = useTemplateRef("day_input");
const day_str = ref("");
@ -44,18 +45,13 @@ function on_click(event: MouseEvent) {
return;
}
if (!editing.value) {
const day_input_focus = () => {
if (day_input.value === null) {
nextTick(day_input_focus);
return;
}
day_input.value.select();
};
day_input_focus();
if (editing.value) {
unwrap_like(model.value).day = day_str.value;
} else {
model.value.day = day_str.value;
wait_for(
() => day_input.value !== null,
() => day_input.value!.select(),
);
}
toggle_editing();
@ -67,7 +63,7 @@ function on_keydown(event: KeyboardEvent) {
}
if (event.key === "Enter") {
model.value.day = day_str.value;
unwrap_like(model.value).day = day_str.value;
toggle_editing();
} else if (event.key === "Delete") {
model.value.day = -1;

View file

@ -1,3 +1,4 @@
import { nextTick, UnwrapRef } from "vue";
import { APIError } from "./api_error";
export function objForEach<T>(
@ -11,6 +12,12 @@ export function objForEach<T>(
}
}
export type Like<T> = T | UnwrapRef<T>;
export function unwrap_like<T>(value: Like<T>): T {
return value as T;
}
export type Loading<T> = T | "loading" | "error";
export function loading_success<T>(o: Loading<T>): o is T {
@ -26,6 +33,18 @@ export function ensure_loaded<T>(o: Loading<T>): T {
return o;
}
export function wait_for(condition: () => boolean, action: () => void) {
const do_action = () => {
if (!condition()) {
nextTick(do_action);
return;
}
action();
};
do_action();
}
export function handle_error(error: unknown) {
if (error instanceof APIError) {
error.alert();

View file

@ -1,4 +1,4 @@
import { UnwrapRef } from "vue";
import { Like, unwrap_like } from "../helpers";
import { DoorSaved } from "../model";
import { Rectangle } from "./rectangle";
import { Vector2D } from "./vector2d";
@ -9,26 +9,27 @@ export class Door {
private _day = Door.MIN_DAY;
public position: Rectangle;
constructor(position: Rectangle);
constructor(position: Rectangle, day: number);
constructor(position: Rectangle, day = Door.MIN_DAY) {
constructor(position: Like<Rectangle>);
constructor(position: Like<Rectangle>, day: number);
constructor(position: Like<Rectangle>, day = Door.MIN_DAY) {
this.day = day;
this.position = position;
this.position = unwrap_like(position);
}
public get day(): number {
return this._day;
}
public set day(day: unknown) {
public set day(value: number | string) {
// integer coercion
const result = Number(day);
let day = Number(value);
if (isNaN(result)) {
this._day = Door.MIN_DAY;
} else {
this._day = Math.max(Math.floor(result), Door.MIN_DAY);
}
day =
!Number.isNaN(day) && Number.isFinite(day)
? Math.trunc(day)
: Door.MIN_DAY;
this._day = Math.max(day, Door.MIN_DAY);
}
public static load(serialized: DoorSaved): Door {
@ -51,5 +52,3 @@ export class Door {
};
}
}
export type VueDoor = UnwrapRef<Door>;

View file

@ -1,4 +1,3 @@
import { UnwrapRef } from "vue";
import { Vector2D } from "./vector2d";
export class Rectangle {
@ -78,5 +77,3 @@ export class Rectangle {
);
}
}
export type VueRectangle = UnwrapRef<Rectangle>;