diff --git a/ui/src/components/CalendarImage.vue b/ui/src/components/CalendarImage.vue
index 9288f65..9cf4e89 100644
--- a/ui/src/components/CalendarImage.vue
+++ b/ui/src/components/CalendarImage.vue
@@ -1,9 +1,185 @@
-
+
+
+
+
\ No newline at end of file
+class Vector2D {
+ public x: number;
+ public y: number;
+
+ constructor(x = 0, y = 0) {
+ this.x = x;
+ this.y = y;
+ }
+
+ public copy(): Vector2D {
+ return new Vector2D(this.x, this.y);
+ }
+
+ public minus(other: Vector2D): Vector2D {
+ return new Vector2D(this.x - other.x, this.y - other.y);
+ }
+}
+
+class Rectangle {
+ private origin: Vector2D;
+ private size: Vector2D;
+
+ constructor(corner1: Vector2D, corner2: Vector2D) {
+ this.origin = corner1.copy();
+ this.size = corner2.minus(corner1);
+ }
+
+ public normalize() {
+ if (this.size.x < 0) {
+ this.size.x *= -1;
+ this.origin.x -= this.size.x;
+ }
+
+ if (this.size.y < 0) {
+ this.size.y *= -1;
+ this.origin.y -= this.size.y;
+ }
+ }
+}
+
+class PreviewState {
+ public visible = false;
+ private down_location = new Vector2D();
+ private move_location = new Vector2D();
+
+ public mouse_down(location: Vector2D) {
+ this.down_location = location;
+ this.move_location = location;
+ }
+
+ public mouse_move(location: Vector2D) {
+ this.move_location = location;
+ }
+
+ public get_rect(): Rectangle {
+ return new Rectangle(this.down_location, this.move_location);
+ }
+}
+
+function get_event_thous(event: MouseEvent): Vector2D {
+ if (event.currentTarget === null) {
+ return new Vector2D();
+ }
+
+ let target = event.currentTarget as Element;
+
+ return new Vector2D(
+ Math.round((event.offsetX / target.clientWidth) * 1000),
+ Math.round((event.offsetY / target.clientHeight) * 1000)
+ );
+}
+
+export default class CalendarImage extends Vue {
+ // "preview" rectangle on click-drag
+
+ private preview_state = new PreviewState();
+
+ private on_mousedown(event: MouseEvent) {
+ this.preview_state.visible = true;
+ this.preview_state.mouse_down(get_event_thous(event));
+ }
+
+ private on_mousemove(event: MouseEvent) {
+ this.preview_state.mouse_move(get_event_thous(event));
+ }
+
+ private on_mouseup() {
+ this.preview_state.visible = false;
+ }
+
+ private get preview_rectangle(): Rectangle {
+ let rect = this.preview_state.get_rect();
+ rect.normalize();
+
+ return rect;
+ }
+
+ // Hook "resize" events
+
+ private resize_observer?: ResizeObserver;
+
+ declare $refs: {
+ container: HTMLDivElement;
+ background: HTMLImageElement;
+ };
+
+ private on_resize() {
+ this.$refs.container.style.setProperty(
+ "height",
+ this.$refs.background.offsetHeight + "px"
+ );
+ }
+
+ public mounted() {
+ this.resize_observer = new ResizeObserver(this.on_resize);
+ this.resize_observer.observe(this.$refs.background);
+ }
+
+ public unmounted() {
+ if (this.resize_observer instanceof ResizeObserver) {
+ this.resize_observer.disconnect();
+ delete this.resize_observer;
+ }
+ }
+}
+
+
+
\ No newline at end of file