Skip to content
Snippets Groups Projects
Commit 61d8f52b authored by Viktoria Wiebe's avatar Viktoria Wiebe Committed by Marcus Eibrink-Lunzenauer
Browse files

CW ImageMapBlock: Implement dragging functionality, closes #1136

Closes #1136

Merge request studip/studip!1088
parent 330ddc50
No related branches found
No related tags found
No related merge requests found
......@@ -110,6 +110,8 @@ $media-buttons: (
next: arr_eol-right
);
$cw-wrapper-gap: 0.5em;
/* * * * * * * *
c o n t e n t s
* * * * * * * * */
......@@ -954,12 +956,13 @@ form.cw-container-dialog-edit-form {
width: 100%;
.cw-block-content {
overflow: auto;
position: relative;
}
}
.cw-content-wrapper-active {
border: solid thin $content-color-40;
.cw-block-content {
padding: 0.5em;
padding: $cw-wrapper-gap;
}
}
.cw-container-wrapper-discuss {
......@@ -1027,6 +1030,27 @@ form.cw-container-dialog-edit-form {
margin-left: 10px;
}
.cw-draggable-shapes-wrapper {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
margin: $cw-wrapper-gap;
.cw-draggable-area {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
&:hover {
cursor: grab;
}
}
}
@media only screen and (max-width: 1820px) {
.cw-structural-element .cw-container-wrapper.cw-container-wrapper-discuss {
max-width: 1095px;
......@@ -1727,6 +1751,7 @@ $icons: (
&.cw-tab-active {
display: block;
height: unset;
padding: 4px 8px;
}
form.default {
......@@ -4069,6 +4094,12 @@ i m a g e m a p b l o c k
.cw-image-map-original-img {
display: none;
}
form.default {
label.cw-block-image-map-dimensions > input[type=number] {
display: inline-block;
}
}
}
/* * * * * * * * * * * * * * * * *
......
<template>
<div class="cw-block cw-block-image-map">
<div class="cw-block cw-block-image-map" @mousedown="selectShape">
<courseware-default-block
:block="block"
:canEdit="canEdit"
......@@ -35,6 +35,38 @@
"
/>
</map>
<div v-if="showEditMode && viewMode === 'edit' && currentShapes.length > 0"
ref="draggableShapeWrapper" class="cw-draggable-shapes-wrapper">
<vue-resizeable
v-for="(shape, index) in currentShapes"
:key="index"
:index="index"
style="position: absolute"
ref="resizableAreaComponents"
:fitParent="true"
:dragSelector="dragSelector"
:active="handlers"
:left="getShapeOffsetLeft(shape)"
:top="getShapeOffsetTop(shape)"
:width="getShapeWidth(shape)"
:height="getShapeHeight(shape)"
@resize:start="dragStartHandler"
@resize:end="endDraggingShape"
@drag:start="dragStartHandler"
@drag:end="endDraggingShape">
<div class="cw-draggable-area"
:style="{
backgroundColor: getColorRGBA(shape.data.color),
color: shape.data.textcolor ? getColorRGBA(shape.data.textcolor) : '',
borderRadius: getShapeBorderRadius(shape),
border: getShapeBorder(shape),
cursor: selectedShapeIndex !== false ? 'grabbing' : '',
}"
@click="followLink(index)">
{{ shape.data.text }}
</div>
</vue-resizeable>
</div>
</template>
<template v-if="canEdit" #edit>
<form class="default" @submit.prevent="">
......@@ -56,7 +88,7 @@
v-for="(shape, index) in currentShapes"
:key="index"
:index="index"
:name="shape.title"
:name="shape.title ? shape.title : ''"
:icon="shape.title === '' ? 'link-extern' : ''"
:selected="index === 0"
>
......@@ -68,6 +100,7 @@
:reduce="color => color.class"
:clearable="false"
v-model="shape.data.color"
@input="drawScreen"
>
<template #open-indicator="selectAttributes">
<span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span>
......@@ -113,6 +146,30 @@
<translate>Beschriftung</translate>
<input type="text" v-model="shape.data.text" @change="drawScreen" />
</label>
<label>
<translate>Textfarbe</translate>
<studip-select
:options="colors"
label="name"
:reduce="color => color.class"
:clearable="false"
v-model="shape.data.textcolor"
@input="drawScreen"
>
<template #open-indicator="selectAttributes">
<span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span>
</template>
<template #no-options>
<translate>Es steht keine Auswahl zur Verfügung.</translate>
</template>
<template #selected-option="{name, rgba}">
<span class="vs__option-color" :style="{'background-color': rgba}"></span><span>{{name}}</span>
</template>
<template #option="{name, rgba}">
<span class="vs__option-color" :style="{'background-color': rgba}"></span><span>{{name}}</span>
</template>
</studip-select>
</label>
<label>
<translate>Art des Links</translate>
<select v-model="shape.link_type">
......@@ -161,6 +218,7 @@ import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
import CoursewareFileChooser from './CoursewareFileChooser.vue';
import CoursewareTabs from './CoursewareTabs.vue';
import CoursewareTab from './CoursewareTab.vue';
import VueResizeable from 'vrp-vue-resizable';
import { blockMixin } from './block-mixin.js';
import { mapActions, mapGetters } from 'vuex';
......@@ -172,6 +230,7 @@ export default {
CoursewareFileChooser,
CoursewareTabs,
CoursewareTab,
VueResizeable,
},
props: {
block: Object,
......@@ -201,7 +260,14 @@ export default {
{ name: this.$gettext('Dunkelgrau'), class: 'darkgrey', rgba: 'rgba(52,73,94,1)' },
{ name: this.$gettext('Schwarz'), class: 'black', rgba: 'rgba(0,0,0,1)' }
],
file: null
file: null,
dragSelector: ".cw-draggable-area",
handlers: ["r", "rb", "b", "lb", "l", "lt", "t", "rt"],
draggedShapeWidth: 50,
draggedShapeHeight: 50,
selectedShapeIndex: false,
draggingActive: false,
showEditMode: false,
};
},
computed: {
......@@ -209,6 +275,7 @@ export default {
courseware: 'courseware-structural-elements/all',
fileRefById: 'file-refs/byId',
urlHelper: 'urlHelper',
viewMode: 'viewMode',
}),
fileId() {
return this.block?.attributes?.payload?.file_id;
......@@ -232,7 +299,8 @@ export default {
updateBlock: 'updateBlockInContainer',
loadFileRef: 'file-refs/loadById',
}),
async initCurrentData() {
async initCurrentData(event) {
this.showEditMode = Boolean(event);
this.currentFileId = this.fileId;
this.currentShapes = JSON.parse(JSON.stringify(this.shapes));
await this.loadFile();
......@@ -307,7 +375,9 @@ export default {
drawShapes() {
let context = this.context;
let view = this;
this.currentShapes.forEach((value) => {
this.currentShapes.forEach((value, index) => {
// skip the selected shape when redrawing so it disappears while dragging the shape
if (this.selectedShapeIndex !== index) {
let shape = value;
let text = shape.data.text;
let shape_width = 0;
......@@ -360,11 +430,15 @@ export default {
text = view.fitTextToShape(context, text, shape_width);
context.textAlign = 'center';
context.font = '14px Arial';
if (shape.data.textcolor) {
context.fillStyle = this.getColorRGBA(shape.data.textcolor);
} else {
if (view.darkColors.indexOf(shape.data.color) > -1) {
context.fillStyle = '#ffffff';
} else {
context.fillStyle = '#000000';
}
}
let lineHeight = shape_height / (text.length + 1);
text.forEach((value, key) => {
context.fillText(value, text_X, text_Y + lineHeight * (key + 1));
......@@ -372,6 +446,7 @@ export default {
}
context.closePath();
}
});
},
fitTextToShape( context , text, shapeWidth) {
......@@ -512,6 +587,7 @@ export default {
},
removeShape(index) {
this.currentShapes.splice(index, 1);
this.drawScreen();
},
fixUrl(index) {
let url = this.currentShapes[index].target_external;
......@@ -520,6 +596,162 @@ export default {
}
this.currentShapes[index].target_external = url;
},
dragStartHandler(data) {
// redraw screen now that a shape was selected so that it disappears while dragging or resizing
this.drawScreen();
},
selectShape(data) {
// set current draggable div shape to canvas shape coordinates
let canvas = this.$refs.image_from_canvas;
let canvasSpecs = canvas.getBoundingClientRect();
let mouseX = (data.clientX - canvasSpecs.left) * (canvas.width/canvasSpecs.width);
let mouseY = (data.clientY - canvasSpecs.top) * (canvas.height/canvasSpecs.height);
this.currentShapes.forEach((value, key) => {
let shape = value;
// if the event target is the draggable area, check for the shape area normally
// else check if the click was on a resizable area that belongs to a shape since
// resizable areas are partly outside the shape
if (data.target.classList.contains('cw-draggable-area')) {
if (this.mouseHit(mouseX, mouseY, shape)) {
this.selectedShapeIndex = key;
}
} else {
mouseX = data.target.parentElement.offsetLeft;
mouseY = data.target.parentElement.offsetTop;
if (shape.type == 'arc') {
mouseX += shape.data.radius;
mouseY += shape.data.radius;
}
if (shape.type == 'rect') {
mouseX += shape.data.width / 2;
mouseY += shape.data.height / 2;
}
if (shape.type == 'ellipse') {
mouseX += shape.data.radiusX;
mouseY += shape.data.radiusY;
}
if (this.mouseHit(mouseX, mouseY, shape)) {
this.selectedShapeIndex = key;
}
}
});
},
endDraggingShape(data) {
this.draggingActive = true;
// transfer div shape data to canvas according to shape
let shape = this.currentShapes[this.selectedShapeIndex];
if (shape.type == 'arc') {
let circle_width = data.width != shape.data.radius * 2? data.width : data.height;
// if the shape was clicked and not dragged, set the dragging status to false to follow the link
if (shape.data.centerX == data.left + shape.data.radius || shape.data.centerY == data.top + shape.data.radius) {
this.draggingActive = false;
}
shape.data.radius = circle_width / 2;
shape.data.centerX = data.left + shape.data.radius;
shape.data.centerY = data.top + shape.data.radius;
}
if (shape.type == 'rect') {
if (shape.data.X == data.left || shape.data.Y == data.top) {
this.draggingActive = false;
}
shape.data.X = data.left;
shape.data.Y = data.top;
shape.data.width = data.width;
shape.data.height = data.height;
}
if (shape.type == 'ellipse') {
if (shape.data.X == data.left + shape.data.radiusX || shape.data.Y == data.top + shape.data.radiusY) {
this.draggingActive = false;
}
shape.data.radiusX = data.width / 2;
shape.data.radiusY = data.height / 2;
shape.data.X = data.left + shape.data.radiusX;
shape.data.Y = data.top + shape.data.radiusY;
}
// unselect shape to stop skipping the selected shape when drawing the canvas
this.selectedShapeIndex = false;
this.drawScreen();
},
mouseHit(mouseX, mouseY, shape) {
// check if the mouseclick was on a shape and return true if it was
if (shape.type == 'arc') {
let dx = shape.data.centerX - mouseX;
let dy = shape.data.centerY - mouseY;
return (dx*dx + dy*dy < shape.data.radius*shape.data.radius);
}
if ((shape.type == 'rect') || (shape.type == 'text')) {
let dx = mouseX - shape.data.X;
let dy = mouseY - shape.data.Y;
return ((dx <= shape.data.width) && (dy <= shape.data.height) && (dx >= 0) && (dy >= 0));
}
if (shape.type == 'ellipse') {
let dx = shape.data.X - mouseX;
let dy = shape.data.Y - mouseY;
return ((Math.abs(dx) < shape.data.radiusX) && (Math.abs(dy) < shape.data.radiusY));
}
},
getColorRGBA(color) {
return this.colors.filter((col) => {return col.class === color})[0].rgba;
},
getShapeBorder(shape) {
return shape.data.color === 'transparent' ? 'dashed thin #000' : 'none';
},
getShapeBorderRadius(shape) {
if (shape.type == 'rect') {
return 0;
} else {
return '50%';
}
},
getShapeOffsetLeft(shape) {
if (shape.type == 'arc') {
return parseInt(shape.data.centerX - shape.data.radius);
}
if (shape.type == 'rect') {
return parseInt(shape.data.X);
}
if (shape.type == 'ellipse') {
return parseInt(shape.data.X) - shape.data.radiusX;
}
},
getShapeOffsetTop(shape) {
if (shape.type == 'arc') {
return parseInt(shape.data.centerY - shape.data.radius);
}
if (shape.type == 'rect') {
return parseInt(shape.data.Y);
}
if (shape.type == 'ellipse') {
return parseInt(shape.data.Y) - shape.data.radiusY;
}
},
getShapeWidth(shape) {
if (shape.type == 'arc') {
return parseInt(shape.data.radius * 2);
}
if (shape.type == 'rect') {
return parseInt(shape.data.width);
}
if (shape.type == 'ellipse') {
return parseInt(shape.data.radiusX * 2);
}
},
getShapeHeight(shape) {
if (shape.type == 'arc') {
return parseInt(shape.data.radius * 2);
}
if (shape.type == 'rect') {
return parseInt(shape.data.height);
}
if (shape.type == 'ellipse') {
return parseInt(shape.data.radiusY * 2);
}
},
followLink(index) {
if (!this.draggingActive) {
this.$refs.map.areas[index].click();
}
},
}
};
</script>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment