Source: components/image-viewer.vue

<template>
    <q-dialog class="q-pa-none q-ma-none fullscreen" v-model="$store.popups.imageViewer.show" @hide="close">
        <div class="fullscreen" @wheel="handleZoom" @touchstart.prevent="null">
            <help-button class="help-button" name="ImageViewer" titleToShow="Image viewer" />
            <q-btn size="md" class="close" dense flat round icon="close" @click="close" @touchstart="close" />
            <q-circular-progress v-if="$store.working" size="48px" indeterminate color="primary" class="nomy center"
                :thickness="0.3" style="z-index: 100;" />
            <q-btn size="xl" v-if="i > 0" class="left" dense flat round icon="arrow_back" @click="backward"
                @touchstart="backward" />
            <q-btn size="xl" v-if="i < files.length - 1" class="right" dense flat round icon="arrow_forward"
                @click="forward" @touchstart="forward" />
            <img ref="img" class="img" :src="src" @mousedown="handleDragStart" @mousemove="handleDragMove"
                @mouseup="handleDragEnd" @selectstart="handleSelectStart" @mouseleave="handleDragEnd"
                @dragstart="handleDefaultDragStart" @touchstart.prevent="onTouchStart" @touchend.prevent="onTouchEnd"
                @touchmove.prevent="onTouchMove" @load="$store.working = false" />
            <compass v-model="compass" :id="file_id" class="compass" v-if="files.length > 0 && files[i].compass != null"
                @update:model-value="compassChanged" />
        </div>
    </q-dialog>
</template>

<script>
import Compass from "./compass.vue";
import HelpButton from "./help-button.vue";

/**
 * Image viewer component
 * @component
 * @name ImageViewer
 * @example
 * <ImageViewer />
 */
export default {
    name: "ImageViewer",
    components: {
        Compass,
        HelpButton
    },
    // props: {
    //     files: { type: Array, default: [] },
    //     index: { type: Number, default: 0 },
    //     needsAuthentication: { type: Boolean, default: false }
    // },
    data() {
        return {
            files: [],
            needsAuthentication: false,
            src: "",
            i: 0,
            img: null,
            scale: 1,
            prevScale: 1,
            prevX: 0,
            prevY: 0,
            dX: 0,
            dY: 0,
            totaldX: 0,
            totaldY: 0,
            prevDist: 0,
            isDragging: false,
            compass: null,
            file_id: null
        };
    },

    methods: {

        /**
         * Displays the image viewer with the specified files and index.
         *
         * @param {Array} files - The array of files to display in the image viewer.
         * @param {number} index - The index of the file to display initially.
         * @param {boolean} needsAuthentication - Indicates whether authentication is required to view the images.
         */
        async show(files, index, needsAuthentication) {
            this.$store.popups.imageViewer.show = true;
            await this.$nextTick();
            this.needsAuthentication = needsAuthentication;
            this.files = files;
            this.i = index;
            this.load();
        },

        /**
         * Moves the image viewer forward to the next image.
         */
        forward() {
            if (this.i < this.files.length - 1) {
                this.i++;
                this.load();
            }
        },

        /**
         * Moves the image viewer backward to the previous image.
         */
        backward() {
            if (this.i > 0) {
                this.i--;
                this.load();
            }
        },

        /**
         * Loads the image from the server.
         */
        async load() {
            this.scale = 1;
            this.dX = 0;
            this.dY = 0;
            this.prevX = 0;
            this.prevY = 0;
            this.totaldX = 0;
            this.totaldY = 0;
            this.isDragging = false;
            this.compass = this.files[this.i].compass;
            this.file_id = this.files[this.i].file_id;
            this.transform();
            this.$store.working = true;
            if (this.needsAuthentication) {
                this.src = await this.getImage(this.files[this.i].url);
            } else {
                this.src = this.files[this.i].url;
            }
        },

        /**
         * Handles the drag start event for the default drag behavior.
         * 
         * @param {Event} event - The drag start event.
         */
        handleDefaultDragStart(event) {
            event.preventDefault();
        },

        /**
         * Handles the select start event.
         *
         * @param {Event} event - The select start event.
         */
        handleSelectStart(event) {
            event.preventDefault();
            return false;
        },

        reset() {
            this.scale = 1;
            this.dX = 0;
            this.dY = 0;
            this.prevX = 0;
            this.prevY = 0;
            this.totaldX = 0;
            this.totaldY = 0;
            this.isDragging = false;
            this.$refs.img.style.cursor = "default";
            this.transform();
        },

        /**
         * Handles the zoom event.
         *
         * @param {Event} event - The zoom event.
         */
        handleZoom(event) {
            event.preventDefault();
            const delta = event.deltaY ? event.deltaY / 1000 : event.scale - 1;
            this.scale += delta;
            this.scale = Math.min(Math.max(1, this.scale), 10); // Limit scale between 1 and 4
            if (this.scale <= 1) {
                this.reset();
            } else {
                this.$refs.img.style.cursor = "grab";
            }
            this.transform();
        },

        shouldChangeImage(dX) {
            if (this.scale > 1) return;
            if (dX < - 100) {
                this.forward();
            } else if (dX > 100) {
                this.backward();
            };
        },

        /**
         * Handles the drag start event.
         * 
         * @param {Event} event - The drag start event.
         */
        handleDragStart(event) {
            this.prevX = event.clientX;
            this.prevY = event.clientY;
            //if (this.scale <= 1) return;
            this.$refs.img.style.cursor = "grabbing";
            this.isDragging = true;
        },

        /**
         * Handles the drag move event.
         *
         * @param {Event} event - The drag move event.
         */
        handleDragMove(event) {
            if (!this.isDragging || this.scale <= 1) return;
            this.dX = event.clientX - this.prevX;
            this.dY = event.clientY - this.prevY;
            this.prevX = event.clientX;
            this.prevY = event.clientY;
            this.totaldX += this.dX;
            this.totaldY += this.dY;
            this.transform();
        },

        /**
         * Handles the drag end event.
         *
         * @param {Event} event - The drag end event.
         */
        handleDragEnd(event) {
            if (this.scale <= 1 && this.isDragging) {
                this.shouldChangeImage(event.clientX - this.prevX);
            }
            this.$refs.img.style.cursor = "default";
            this.isDragging = false;
        },

        /**
         * Sets CSS image transformation depending on scale and drag.
         */
        transform() {
            this.$refs.img.style.transform = `scale(${this.scale}) translate(${this.totaldX}px, ${this.totaldY}px)`;
        },

        /**
         * Closes the image viewer.
         */
        close() {
            this.$store.popups.imageViewer.show = false;
        },

        onTouchStart(ev) {
            if (ev.touches.length === 1) {
                this.prevX = ev.touches[0].clientX
                this.prevY = ev.touches[0].clientY
                this.isDragging = true;
            } else if (ev.touches.length === 2) {
                let distX = ev.touches[0].clientX - ev.touches[1].clientX
                let distY = ev.touches[0].clientY - ev.touches[1].clientY
                this.prevDist = Math.sqrt(distX * distX + distY * distY)
            }
        },

        onTouchMove(ev) {
            if (ev.touches.length === 1 && this.isDragging) {
                this.dX = ev.touches[0].clientX - this.prevX;
                this.dY = ev.touches[0].clientY - this.prevY;
                if (this.scale <= 1) return;
                this.prevX = ev.touches[0].clientX;
                this.prevY = ev.touches[0].clientY;
                this.totaldX += this.dX;
                this.totaldY += this.dY;
                this.transform();
                // this.onPointerMove(ev.touches[0].clientX, ev.touches[0].clientY)
            } else if (ev.touches.length === 2) {
                let distX = ev.touches[0].clientX - ev.touches[1].clientX;
                let distY = ev.touches[0].clientY - ev.touches[1].clientY;
                let dist = Math.sqrt(distX * distX + distY * distY);
                this.scale *= dist / this.prevDist;
                this.scale = Math.min(Math.max(1, this.scale), 10);
                if (this.scale <= 1) {
                    this.reset();
                }
                this.prevDist = dist;
                this.transform();
            }
        },
        onTouchEnd(ev) {
            this.isDragging = false;
            if (this.scale <= 1) {
                this.shouldChangeImage(this.dX);
            }
        },
        compassChanged(val) {
            console.log("compassChanged", val);
            this.compass = val;
            this.files[this.i].compass = val;
            this.$emit("compassChanged", this.compass, this.files[this.i].file_id);
        }
    }
}
</script>
<style scoped>
.help-button {
    position: absolute;
    top: 5px;
    right: 35px;
    z-index: 200;
    cursor: pointer;
}

.close {
    position: absolute;
    top: 2px;
    right: 2px;
    z-index: 200;
    cursor: pointer;
    color: black;
}

.left {
    position: absolute;
    left: 3px;
    z-index: 100;
    color: black
}

.right {
    position: absolute;
    right: 3px;
    z-index: 100;
    color: black
}

.center {
    position: absolute;
    z-index: 100;
    color: black
}

.compass {
    position: absolute;
    left: 3px;
    top: 3px;
    z-index: 100;
    color: black
}

.fullscreen {
    max-width: none !important;
    max-height: none !important;
    width: 100vw !important;
    height: 100vh !important;
    background-color: white !important;
    display: flex;
    justify-content: center;
    align-items: center;
    overflow: hidden;
}

.img {
    max-width: 100% !important;
    max-height: 100% !important;
    width: auto !important;
    height: auto !important;
    z-index: 0
}
</style>