Skip to content
Snippets Groups Projects
Select Git revision
  • 7293abbad9c1a149cfffd99c9ab5060fe945b773
  • main default protected
  • step-3263
  • feature/plugins-cli
  • feature/vite
  • step-2484-peerreview
  • biest/issue-5051
  • tests/simplify-jsonapi-tests
  • fix/typo-in-1a70031
  • feature/broadcasting
  • database-seeders-and-factories
  • feature/peer-review-2
  • feature-feedback-jsonapi
  • feature/peerreview
  • feature/balloon-plus
  • feature/stock-images-unsplash
  • tic-2588
  • 5.0
  • 5.2
  • biest/unlock-blocks
  • biest-1514
21 results

Unit.php

Blame
  • Forked from Stud.IP / Stud.IP
    Source project has a limited visibility.
    Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    CoursewareCanvasBlock.vue 27.87 KiB
    <template>
        <div class="cw-block cw-block-canvas" ref="block">
            <courseware-default-block
                :block="block"
                :canEdit="canEdit"
                :isTeacher="isTeacher"
                :preview="true"
                @showEdit="initCurrentData"
                @storeEdit="storeBlock"
                @closeEdit="initCurrentData"
            >
                <template #content>
                    <div v-if="currentTitle" class="cw-block-title">
                        {{ currentTitle }}
                    </div>
                    <div class="cw-canvasblock-toolbar">
                        <div class="cw-canvasblock-buttonset">
                            <button class="cw-canvasblock-reset" :title="$gettext('Zurücksetzen')" @click="reset"></button>
                            <button class="cw-canvasblock-undo" :title="$gettext('Rückgängig')" @click="undo"></button>
                            <button v-if="hasUploadFolder" class="cw-canvasblock-store" :title="$gettext('Bild im Dateibereich speichern')" @click="store"></button>
                            <button v-if="canSwitchView" :class="viewButtonClass" :title="viewButtonText" @click="switchView"></button>
                        </div>
                        <div class="cw-canvasblock-buttonset">
                            <button
                                v-for="color in colors"
                                :key="color.name"
                                class="cw-canvasblock-color"
                                :class="[currentColor === color.name ? 'selected-color' : '', color.name]"
                                :title="color.title"
                                @click="setColor(color.name)"
                            />
                        </div>
                        <div class="cw-canvasblock-buttonset">
                            <button
                                class="cw-canvasblock-size cw-canvasblock-size-small"
                                :class="{ 'selected-size': currentSize === 2 }"
                                :title="$gettext('klein')"
                                @click="setSize('small')"
                            />
                            <button
                                class="cw-canvasblock-size cw-canvasblock-size-normal"
                                :class="{ 'selected-size': currentSize === 5 }"
                                :title="$gettext('normal')"
                                @click="setSize('normal')"
                            />
                            <button
                                class="cw-canvasblock-size cw-canvasblock-size-large"
                                :class="{ 'selected-size': currentSize === 8 }"
                                :title="$gettext('groß')"
                                @click="setSize('large')"
                            />
                            <button
                                class="cw-canvasblock-size cw-canvasblock-size-huge"
                                :class="{ 'selected-size': currentSize === 12 }"
                                :title="$gettext('riesig')"
                                @click="setSize('huge')"
                            />
                        </div>
                        <div class="cw-canvasblock-buttonset">
                            <button
                                class="cw-canvasblock-tool cw-canvasblock-tool-pen"
                                :class="{ 'selected-tool': currentTool === 'pen' }"
                                :title="$gettext('Zeichenwerkzeug')"
                                @click="setTool('pen')"
                            />
                            <button
                                class="cw-canvasblock-tool cw-canvasblock-tool-text"
                                :class="{ 'selected-tool': currentTool === 'text' }"
                                :title="$gettext('Textwerkzeug')"
                                @click="setTool('text')"
                            >
                                T
                            </button>
                        </div>
                    </div>
                    <img :src="currentUrl" class="cw-canvasblock-original-img" ref="image" @load="buildCanvas" />
                    <input
                        v-show="textInput"
                        class="cw-canvasblock-text-input"
                        ref="textInputField"
                        @keyup="textInputKeyUp"
                    />
                    <canvas
                        class="cw-canvasblock-canvas"
                        :class="{
                            'cw-canvasblock-tool-selected-pen': currentTool === 'pen',
                            'cw-canvasblock-tool-selected-text': currentTool === 'text',
                        }"
                        ref="canvas"
                        @mousedown="mouseDown"
                        @mousemove="mouseMove"
                        @mouseup="mouseUp"
                        @mouseout="mouseUp"
                        @mouseleave="mouseUp"
    
                        @touchstart="touchStart"
                        @touchmove="touchMove"
                        @touchend="touchEnd"
                    />
                    <div class="cw-canvasblock-hints">
                        <div v-show="write" class="messagebox messagebox_info cw-canvasblock-text-info">
                            <translate>Texteingabe mit Enter-Taste bestätigen</translate>
                        </div>
                    </div>
                </template>
                <template v-if="canEdit" #edit>
                    <form class="default" @submit.prevent="">
                        <label>
                            <translate>Überschrift</translate>
                            <input type="text" v-model="currentTitle" />
                        </label>
                        <label>
                            <translate>Hintergrundbild</translate>
                            <select v-model="currentImage">
                                <option value="true"><translate>Ja</translate></option>
                                <option value="false"><translate>Nein</translate></option>
                            </select>
                        </label>
                        <label v-if="currentImage === 'true'">
                            <translate>Bilddatei</translate>
                            <courseware-file-chooser
                                v-model="currentFileId"
                                :isImage="true"
                                @selectFile="updateCurrentFile"
                            />
                        </label>
                        <label>
                            <translate>Speicherort</translate>
                            <courseware-folder-chooser v-model="currentUploadFolderId" :unchoose="true"/>
                        </label>
                        <label>
                            <translate>Werte anderer Nutzer anzeigen</translate>
                            <select v-model="currentShowUserData">
                                <option value="off"><translate>deaktiviert</translate></option>
                                <option value="teacher"><translate>nur für Lehrende</translate></option>
                                <option value="all"><translate>für alle</translate></option>
                            </select>
                        </label>
                    </form>
                </template>
                <template #info>
                    <p><translate>Informationen zum Leinwand-Block</translate></p>
                </template>
            </courseware-default-block>
        </div>
    </template>
    
    <script>
    import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
    import CoursewareFileChooser from './CoursewareFileChooser.vue';
    import CoursewareFolderChooser from './CoursewareFolderChooser.vue';
    import { blockMixin } from './block-mixin.js';
    import { mapActions, mapGetters } from 'vuex';
    
    export default {
        name: 'courseware-canvas-block',
        mixins: [blockMixin],
        components: {
            CoursewareDefaultBlock,
            CoursewareFileChooser,
            CoursewareFolderChooser,
        },
        props: {
            block: Object,
            canEdit: Boolean,
            isTeacher: Boolean,
        },
        data() {
            return {
                currentTitle: '',
                currentImage: '',
                currentFileId: '',
                currentUploadFolderId: '',
                currentShowUserData: '',
                currentUserView: 'own',
                currentFile: {},
    
                context: {},
                paint: false,
                write: false,
                clickX: [],
                clickY: [],
                clickDrag: [],
                clickColor: [],
                colors: [
                    {rgba: 'rgba(255,255,255,1)', title: this.$gettext('weiß'), name: 'white'},
                    {rgba: 'rgba(52,152,219,1)', title: this.$gettext('blau'), name: 'blue'},
                    {rgba: 'rgba(46,204,113,1)', title: this.$gettext('grün'), name: 'green'},
                    {rgba: 'rgba(155,89,182,1)', title: this.$gettext('lila'), name: 'purple'},
                    {rgba: 'rgba(231,76,60,1)', title: this.$gettext('rot'), name: 'red'},
                    {rgba: 'rgba(254,211,48,1)', title: this.$gettext('gelb'), name: 'yellow'},
                    {rgba: 'rgba(243,156,18,1)', title: this.$gettext('orange'), name: 'orange'},
                    {rgba: 'rgba(149,165,166,1)', title: this.$gettext('grau'), name: 'grey'},
                    {rgba: 'rgba(52,73,94,1)', title: this.$gettext('dunkel grau'), name: 'darkgrey'},
                    {rgba: 'rgba(0,0,0,1)', title: this.$gettext('schwarz'), name: 'black'},
                ],
                currentColor: '',
                currentColorRGBA: '',
                sizes: { small: 2, normal: 5, large: 8, huge: 12 },
                clickSize: [],
                currentSize: '',
                tools: { pen: 'pen', text: 'text' },
                currentTool: '',
                clickTool: [],
                Text: [],
                textInput: false,
                file: null
            };
        },
        computed: {
            ...mapGetters({
                userId: 'userId',
                getUserDataById: 'courseware-user-data-fields/byId',
                usersById: 'users/byId',
                relatedUserData: 'user-data-field/related'
            }),
            userData() {
                return this.getUserDataById({ id: this.block.relationships['user-data-field'].data.id });
            },
            canvasDraw() {
                if (this.userData !== undefined && this.userData.attributes.payload.canvas_draw) {
                    return this.userData.attributes.payload.canvas_draw;
                } else {
                    return false;
                }
            },
            allCanvasDraws() {
                const parent = { type: 'courseware-blocks', id: this.block.id };
                const relationship = 'user-data-field';
                const userDataFields = this.relatedUserData({
                    parent: parent,
                    relationship: relationship,
                });
                let canvasDraws = [];
                if (userDataFields?.length > 0) {
                    for (let userDataField of userDataFields) {
                        // extracting the canvas draws of the other users.
                        if (userDataField?.attributes?.payload?.canvas_draw &&
                            userDataField?.relationships?.user?.data?.id !== this.userId ) {
                            let canvas_draw = userDataField.attributes.payload.canvas_draw;
                            let draw_obj = {
                                clickX: JSON.parse(canvas_draw.clickX),
                                clickY: JSON.parse(canvas_draw.clickY),
                                clickDrag: JSON.parse(canvas_draw.clickDrag),
                                clickColor: JSON.parse(canvas_draw.clickColor),
                                clickSize: JSON.parse(canvas_draw.clickSize),
                                clickTool: JSON.parse(canvas_draw.clickTool),
                                Text: JSON.parse(canvas_draw.Text),
                            };
                            canvasDraws.push(draw_obj);
                        }
                    }
                }
                return canvasDraws;
            },
            title() {
                return this.block?.attributes?.payload?.title;
            },
            image() {
                return this.block?.attributes?.payload?.image;
            },
            fileId() {
                return this.block?.attributes?.payload?.file_id;
            },
            uploadFolderId() {
                return this.block?.attributes?.payload?.upload_folder_id;
            },
            showUsersData() {
                return this.block?.attributes?.payload?.show_usersdata;
            },
            currentUrl() {
                if (this.currentFile?.meta) {
                    return this.currentFile.meta['download-url'];
                } else if(this.currentFile?.download_url) {
                        return this.currentFile.download_url;
                } else {
                    return '';
                }
            },
            currentFileName() {
                if (this.currentFile?.attributes?.name) {
                    return this.currentFile.attributes.name;
                } else {
                    return this.currentTitle + '.jpg';
                }
            },
            hasUploadFolder() {
                return this.currentUploadFolderId !== "";
            },
            canSwitchView() {
                // this feature is not something to offer in the Arbeitsplatz!
                let context = this.$store.getters.context;
                if (context.type !== 'courses') {
                    return false;
                }
                if (this.currentShowUserData === 'off') {
                    return false;
                }
                if (this.currentShowUserData === 'teacher' && !this.isTeacher) {
                    return false;
                }
                return true;
            },
            viewButtonText() {
                let text = this.$gettext('Werte anderer Nutzer anzeigen');
                if (this.currentUserView == 'own') {
                    text = this.$gettext('Nur eigene Werte anzeigen');
                }
                return text;
            },
            viewButtonClass() {
                return 'cw-canvasblock-show-' + this.currentUserView;
            }
        },
        mounted() {
            this.loadFileRefs(this.block.id).then((response) => {
                this.file = response[0];
                this.currentFile = this.file;
                this.initCurrentData();
                this.buildCanvas();
            });
            this.loadImageFile();
        },
        methods: {
            ...mapActions({
                updateBlock: 'updateBlockInContainer',
                loadFileRefs: 'loadFileRefs',
                createFile: 'createFile',
                companionSuccess: 'companionSuccess',
                companionError: 'companionError',
                updateUserDataFields: 'courseware-user-data-fields/update',
                loadUserDataFields: 'loadUserDataFields',
            }),
            initCurrentData() {
                this.currentTitle = this.title;
                this.currentImage = this.image;
                this.currentFileId = this.fileId;
                this.currentUploadFolderId = this.uploadFolderId;
                this.currentShowUserData = this.showUsersData;
                if (this.canvasDraw) {
                    this.clickX = JSON.parse(this.canvasDraw.clickX);
                    this.clickY = JSON.parse(this.canvasDraw.clickY);
                    this.clickDrag = JSON.parse(this.canvasDraw.clickDrag);
                    this.clickColor = JSON.parse(this.canvasDraw.clickColor);
                    this.clickSize = JSON.parse(this.canvasDraw.clickSize);
                    this.clickTool = JSON.parse(this.canvasDraw.clickTool);
                    this.Text = JSON.parse(this.canvasDraw.Text);
                }
            },
            loadImageFile() {
                this.loadFileRefs(this.block.id).then((response) => {
                    this.file = response[0];
                    this.currentFile = this.file;
                    this.initCurrentData();
                    this.buildCanvas();
                });
            },
            updateCurrentFile(file) {
                this.currentFile = file;
                this.currentFileId = file.id;
                this.buildCanvas();
            },
            setColor(color) {
                if (this.write) {
                    return;
                }
                this.currentColor = color;
                this.currentColorRGBA = this.colors.find(c => c.name === color).rgba;
            },
            setSize(size) {
                if (this.textInput) {
                    return;
                }
                this.currentSize = this.sizes[size];
            },
            setTool(tool) {
                if (this.write) {
                    this.clickX.pop();
                    this.clickY.pop();
                    this.clickDrag.pop();
                    this.clickColor.pop();
                    this.clickSize.pop();
                    this.clickTool.pop();
                    this.write = false;
                    this.textInput = false;
                }
                this.currentTool = this.tools[tool];
            },
            reset() {
                this.clickX.length = 0;
                this.clickY.length = 0;
                this.clickDrag.length = 0;
                this.clickColor.length = 0;
                this.clickSize.length = 0;
                this.clickTool.length = 0;
                this.Text.length = 0;
                this.paint = false;
                this.write = false;
                this.textInput = false;
                this.redraw();
            },
            buildCanvas() {
                let blockElem = this.$refs.block;
                let image = this.$refs.image;
                let canvas = this.$refs.canvas;
                canvas.width = blockElem.offsetWidth - 2;
                if (this.currentImage === 'true' && image.height > 0) {
                    canvas.height = Math.round((canvas.width / image.width) * image.height);
                } else {
                    canvas.height = 500;
                }
                this.context = canvas.getContext('2d');
                this.setColor('blue');
                this.currentSize = this.sizes['normal'];
                this.currentTool = this.tools['pen'];
                this.redraw();
            },
            redraw() {
                let view = this;
                let context = view.context;
                context.clearRect(0, 0, context.canvas.width, context.canvas.height); // Clears the canvas
                context.fillStyle = '#ffffff';
                context.fillRect(0, 0, context.canvas.width, context.canvas.height); // set background
                if (view.currentImage === 'true') {
                    let outlineImage = new Image();
                    outlineImage.src = this.currentUrl;
                    context.drawImage(outlineImage, 0, 0, context.canvas.width, context.canvas.height);
                }
    
                context.lineJoin = 'round';
                let ownCanvasDraw = {
                    clickX: view.clickX,
                    clickY: view.clickY,
                    clickDrag: view.clickDrag,
                    clickColor: view.clickColor,
                    clickSize: view.clickSize,
                    clickTool: view.clickTool,
                    Text: view.Text
                }
                let canvasDraws = [ownCanvasDraw];
                if (this.currentUserView === 'all') {
                    canvasDraws = [ ...canvasDraws, ...view.allCanvasDraws ];
                }
    
                for (let draw of canvasDraws) {
                    for (var j = 0; j < draw.clickX.length; j++) {
                        if (draw.clickTool[j] === 'pen') {
                            context.beginPath();
                            if (draw.clickDrag[j] && j) {
                                context.moveTo(draw.clickX[j - 1], draw.clickY[j - 1]);
                            } else {
                                context.moveTo(draw.clickX[j] - 1, draw.clickY[j]);
                            }
                            context.lineTo(draw.clickX[j], draw.clickY[j]);
                            context.closePath();
                            context.strokeStyle = draw.clickColor[j];
                            context.lineWidth = draw.clickSize[j];
                            context.stroke();
                        }
                        if (draw.clickTool[j] === 'text') {
                            let fontsize = draw.clickSize[j] * 6;
                            context.font = fontsize + 'px Arial ';
                            context.fillStyle = draw.clickColor[j];
                            context.fillText(draw.Text[j], draw.clickX[j], draw.clickY[j] + fontsize);
                        }
                    }
                }
            },
            mouseDown(e) {
                if (this.write) {
                    let view = this;
                    this.$refs.textInputField.focus();
                    window.setTimeout(function () {
                        view.$refs.textInputField.focus();
                    }, 0);
                    return;
                }
                if (this.currentTool === 'pen') {
                    this.paint = true;
                    this.addClick(e.offsetX, e.offsetY, false);
                    this.redraw();
                }
                if (this.currentTool === 'text') {
                    this.write = true;
                    this.addClick(e.offsetX, e.offsetY, false);
                }
            },
            mouseMove(e) {
                if (this.paint) {
                    this.addClick(e.offsetX, e.offsetY, true);
                    this.redraw();
                }
            },
            mouseUp(e) {
                this.storeDraw();
                this.paint = false;
            },
            touchStart(e) {
                e.preventDefault();
                if (this.write) {
                    return;
                }
                let canvas = this.$refs.canvas;
                let mousePos = this.getTouchPos(canvas, e);
                if(this.currentTool == 'pen') {
                    this.paint = true;
                    this.addClick(mousePos.x, mousePos.y, false);
                    this.redraw();
                }
                if(this.currentTool == 'text') {
                    this.write = true;
                    this.addClick(mousePos.x, mousePos.y, false);
                }
            },
            touchMove(e) {
                e.preventDefault();
    
                let canvas = this.$refs.canvas;
                let mousePos = this.getTouchPos(canvas, e);
                if(this.paint){
                    this.addClick(mousePos.x, mousePos.y, true);
                    this.redraw();
                }
            },
            touchEnd(e) {
                this.storeDraw();
                this.paint = false;
            },
            getTouchPos(canvasDom, touchEvent) {
                var rect = canvasDom.getBoundingClientRect();
                return {
                    x: touchEvent.touches[0].clientX - rect.left,
                    y: touchEvent.touches[0].clientY - rect.top
                };
            },
            addClick(x, y, dragging) {
                this.clickX.push(x);
                this.clickY.push(y);
                this.clickDrag.push(dragging);
                this.clickColor.push(this.currentColorRGBA);
                this.clickSize.push(this.currentSize);
                this.clickTool.push(this.currentTool);
                if (this.currentTool === 'text') {
                    this.enableTextInput(x, y);
                } else {
                    this.Text.push('');
                }
            },
            undo() {
                let dragging = this.clickDrag[this.clickDrag.length - 1];
                this.clickX.pop();
                this.clickY.pop();
                this.clickDrag.pop();
                this.clickColor.pop();
                this.clickSize.pop();
                this.clickTool.pop();
                if (this.write) {
                    this.textInput = false;
                    this.write = false;
                } else {
                    this.Text.pop('');
                }
                if (dragging) {
                    this.undo();
                }
                this.redraw();
            },
            enableTextInput(x, y) {
                let view = this;
                let fontsize = this.currentSize * 6;
                this.textInput = true;
                let input = this.$refs.textInputField;
                input.value = '';
                input.style.position = 'absolute';
                input.style.top = this.$refs.canvas.offsetTop + y + 'px';
                input.style.left = 320 + x + 'px';
                input.style.lineHeight = fontsize + 'px';
                input.style.fontSize = fontsize + 'px';
                input.style.width = '300px';
                window.setTimeout(function () {
                    view.$refs.textInputField.focus();
                }, 0);
            },
            textInputKeyUp(e) {
                if (e.defaultPrevented) {
                    return;
                }
                let key = e.key || e.keyCode;
                if (key === 'Enter' || key === 13) {
                    this.Text.push(this.$refs.textInputField.value);
                    this.textInput = false;
                    this.write = false;
                    this.redraw();
                }
                if (key === 'Escape' || key === 'Esc' || key === 27) {
                    this.clickX.pop();
                    this.clickY.pop();
                    this.clickDrag.pop();
                    this.clickColor.pop();
                    this.clickSize.pop();
                    this.clickTool.pop();
                    this.textInput = false;
                    this.write = false;
                }
            },
            async storeDraw() {
                let data = {};
                data.type = 'courseware-user-data-fields';
                data.id = this.block.relationships['user-data-field'].data.id;
                data.relationships = {};
                data.relationships.block = {};
                data.relationships.block.data = {};
                data.relationships.block.data.id = this.block.id;
                data.relationships.block.data.type = this.block.type;
                data.attributes = {};
                data.attributes.payload = {};
                data.attributes.payload.canvas_draw = {};
                data.attributes.payload.canvas_draw.clickX = JSON.stringify(this.clickX);
                data.attributes.payload.canvas_draw.clickY = JSON.stringify(this.clickY);
                data.attributes.payload.canvas_draw.clickDrag = JSON.stringify(this.clickDrag);
                data.attributes.payload.canvas_draw.clickColor = JSON.stringify(this.clickColor);
                data.attributes.payload.canvas_draw.clickSize = JSON.stringify(this.clickSize);
                data.attributes.payload.canvas_draw.clickTool = JSON.stringify(this.clickTool);
                data.attributes.payload.canvas_draw.Text = JSON.stringify(this.Text);
    
                await this.updateUserDataFields(data);
            },
            storeBlock() {
                let attributes = {};
                attributes.payload = {};
                attributes.payload.title = this.currentTitle;
                attributes.payload.image = this.currentImage;
                if (this.currentImage === 'true') {
                    attributes.payload.file_id = this.currentFileId;
                } else {
                    attributes.payload.file_id = '';
                }
                attributes.payload.upload_folder_id = this.currentUploadFolderId;
                attributes.payload.show_usersdata = this.currentShowUserData;
    
                this.updateBlock({
                    attributes: attributes,
                    blockId: this.block.id,
                    containerId: this.block.relationships.container.data.id,
                });
            },
            async store() {
                let user = this.usersById({id: this.userId});
                let imageBase64 = this.context.canvas.toDataURL("image/jpeg", 1.0);
                let image = await fetch(imageBase64);
                let imageBlob = await image.blob();
                let file = {};
                file.attributes = {};
                if(this.currentImage === 'true') {
                    file.attributes.name = (user.attributes["formatted-name"]).replace(/\s+/g, '_') + '_' + this.currentFile.attributes.name;
                } else {
                    file.attributes.name = (user.attributes["formatted-name"]).replace(/\s+/g, '_') + '_' + this.block.attributes.title + '_' + this.block.id;
                }
    
                let img = await this.createFile({
                    file: file,
                    filedata: imageBlob,
                    folder: {id: this.currentUploadFolderId}
                });
    
                if(img && img.type === 'file-refs') {
                    this.companionSuccess({
                        info: this.$gettext('Das Bild wurde erfolgreich im Dateibereich abgelegt.')
                    });
                } else {
                    this.companionError({
                        info: this.$gettext('Es ist ein Fehler aufgetretten! Das Bild konnte nicht gespeichert werden.')
                    });
                }
            },
            async switchView() {
                if (['own', 'all'].includes(this.currentUserView)) {
                    let newView = 'own';
                    if (this.currentUserView === 'own') {
                        // we will get the latest draws by loading them each time the view is going to be switched to all!
                        await this.loadUserDataFields(this.block.id).then(() => newView = 'all');
                    }
                    this.currentUserView = newView;
                }
            }
        },
        watch: {
            currentUserView() {
                this.redraw();
            }
        },
    };
    </script>