This commit is contained in:
Ivymaster
2026-09-09 16:53:23 +02:00
parent 00603ef52d
commit ccb33cde60
7 changed files with 196 additions and 53 deletions
+21
View File
@@ -109,3 +109,24 @@
z-index: 2;
}
.fhc-calendar-mode-range .fhc-calendar-base-grid-line-event {
position: relative;
}
.fhc-calendar-mode-range .fhc-resize-bar {
position: absolute;
top: 0;
bottom: 0;
height: auto;
width: 15px;
margin: 0;
cursor: ew-resize;
}
.fhc-calendar-mode-range .fhc-resize-bar--left {
left: 0;
}
.fhc-calendar-mode-range .fhc-resize-bar--right {
right: 0;
}
+3 -2
View File
@@ -479,7 +479,7 @@ export default {
ctrlKey: !!(evt?.ctrlKey || evt?.metaKey)
});
},
handleResizeStart({ edge, evt, el, event })
handleResizeStart({ edge, evt, horizontal, el, event })
{
const gridEl = this.$refs.body;
@@ -490,6 +490,7 @@ export default {
el,
gridEl,
event,
horizontal,
timeGrid: this.timeGrid,
onEnd: ({ event, newStart, newEnd }) => {
const orig = event?.orig;
@@ -650,4 +651,4 @@ export default {
</div>
</div>
`
}
}
@@ -94,6 +94,7 @@ export default {
this.$emit('resize-start', {
edge,
evt,
horizontal: this.mode === 'range',
el: this.$refs.eventEl,
event: this.event
});
@@ -153,7 +154,7 @@ export default {
data-cy="calendar-event"
>
<div
v-if="resizable"
v-if="resizable && mode !== 'range'"
class="fhc-resize-bar fhc-resize-bar--top"
@pointerdown.prevent.stop="onResizeStart('start', $event)"
@click.stop
@@ -164,15 +165,30 @@ export default {
{{ event.orig }}
</slot>
<div
v-if="resizable"
v-if="resizable && mode !== 'range'"
class="fhc-resize-bar fhc-resize-bar--bottom"
@pointerdown.prevent.stop="onResizeStart('end', $event)"
@click.stop
>
<i class="fa-solid fa-grip-lines text-muted"></i>
</div>
<div
v-if="resizable && mode === 'range'"
class="fhc-resize-bar fhc-resize-bar--left"
@pointerdown.prevent.stop="onResizeStart('start', $event)"
@click.stop
>
<i class="fa-solid fa-grip-lines-vertical text-muted"></i>
</div>
<div
v-if="resizable && mode === 'range'"
class="fhc-resize-bar fhc-resize-bar--right"
@pointerdown.prevent.stop="onResizeStart('end', $event)"
@click.stop
>
<i class="fa-solid fa-grip-lines-vertical text-muted"></i>
</div>
<teleport to="body">
<div
v-if="contextMenu.show"
@@ -196,4 +212,4 @@ export default {
</teleport>
</div>
`
}
}
+45 -5
View File
@@ -5,15 +5,22 @@ import { useEventLoader } from '../../composables/EventLoader.js';
import ModeWeek from './Mode/Week.js';
import ModeMonth from './Mode/Month.js';
import ModeTable from './Mode/Table.js';
import ModeRange from './Mode/Range.js';
import ApiKalender from '../../api/factory/tempus/kalender.js';
import draggable from '../../directives/draggable.js';
import ApiStudiensemester from '../../api/factory/studiensemester.js';
export default {
name: "CalendarTempus",
components: {
FhcCalendar
},
provide() {
return {
rangeLength: Vue.computed(() => this.rangeLength),
rangeViewPresets: Vue.computed(() => this.semesterRangePresets)
};
},
inject: {
renderers: {from: 'renderers'},
canToggleGrid: {
@@ -25,7 +32,14 @@ export default {
default: {
visible_status: 'all'
}
}
},
rangeLength: {
default: 1,
},
shouldIncludeRangeMode: {
type: Boolean,
default: false,
},
},
directives: {
draggable,
@@ -79,13 +93,13 @@ export default {
"event-unhover",
"open-reservierung"
],
data() {
return {
modes: {
week: Vue.markRaw(ModeWeek),
month: Vue.markRaw(ModeMonth),
tableList: Vue.markRaw(ModeTable),
range: Vue.markRaw(ModeRange),
},
modeOptions: {
day: {
@@ -100,6 +114,10 @@ export default {
teachingunits: null,
hoursplan: null,
showRaster: true,
semesterRangePresets: {
label: null,
presets: [],
},
};
},
computed: {
@@ -114,7 +132,9 @@ export default {
end: now.startOf('day')
}];
}
else
else if (this.mode == 'Range') {
return [];
} else
{
past = [{
class: 'background-past',
@@ -183,6 +203,24 @@ export default {
navigateNext() {
this.$refs.calendar.clickNext();
},
async fetchSemesters() {
const semestersResponse = await this.$api.call(ApiStudiensemester.getAll());
if (semestersResponse.meta.status === "success") {
this.semesterRangePresets = {
label: "View specific semester",
presets: semestersResponse.data.map((semester) => {
let startDate = luxon.DateTime.fromISO(semester.start);
let endDate = luxon.DateTime.fromISO(semester.ende);
return {
startDate,
endDate,
name: semester.studiensemester_kurzbz,
description: semester.bezeichnung,
};
})
};
}
},
},
setup(props, context) {
const rangeInterval = Vue.ref(null);
@@ -220,6 +258,8 @@ export default {
end: res.data.end
};
});
this.fetchSemesters();
},
template: /* html */`
<fhc-calendar
@@ -238,7 +278,7 @@ export default {
show-btns
:draggable-events="true"
:resizable-events="true"
:on-drop="currentMode === 'week' ? ondrop : null"
:on-drop="['week', 'range'].includes(currentMode) ? ondrop : null"
:on-resize="onresize"
@update:date="(newDate, newMode) => $emit('update:date', newDate, newMode)"
@update:mode="(newMode, newDate) => { currentMode = newMode; $emit('update:mode', newMode, newDate) }"
@@ -1,4 +1,10 @@
export default {
inject: {
mode: {
from: 'mode',
default: null,
},
},
props: {
event: {
type: Object,
@@ -148,19 +154,6 @@ export default {
class="position-relative"
@wheel.stop
>
<div class="position-absolute top-0 start-0 m-1 d-flex gap-1" >
<i :class="statusIcon"></i>
<i
v-tooltip="resourcesTooltip"
v-if="event.has_assigned_resources"
class="fa-solid fa-table-list text-muted"
></i>
<i
v-tooltip="tagsTooltip"
v-if="tags?.length"
class="fa-solid fa-tags"
></i>
</div>
<div class="position-absolute bottom-0 start-0 m-1">
{{event.verplante_stunden}}
</div>
@@ -172,7 +165,28 @@ export default {
<span>{{ end }}</span>
</div>
<div class="event-text" v-tooltip="tooltipString">
<span class="event-topic">{{ topicString }}</span>
<div
:class="{ 'd-flex align-items-center gap-1': mode === 'range' }"
:style="mode === 'range' ? null : { display: 'contents' }"
>
<span class="event-topic">{{ topicString }}</span>
<div
class="d-flex gap-1"
:class="{ 'position-absolute top-0 start-0 m-1': mode !== 'range' }"
>
<i :class="statusIcon"></i>
<i
v-if="event.has_assigned_resources"
v-tooltip="resourcesTooltip"
class="fa-solid fa-table-list text-muted"
></i>
<i
v-if="tags?.length"
v-tooltip="tagsTooltip"
class="fa-solid fa-tags"
></i>
</div>
</div>
<span
class="event-place"
data-cy="calendar-event-room"
+13 -5
View File
@@ -59,7 +59,9 @@ export function useResizeGhost() {
return {
startTop: topInGrid,
startHeight: eventRect.height
startHeight: eventRect.height,
startLeft: leftInGrid,
startWidth: eventRect.width
};
}
@@ -69,7 +71,7 @@ export function useResizeGhost() {
labelEl.textContent = text;
}
function updatePosition(top, height)
function updatePosition(top, height, width, left)
{
if (!ghostEl)
return;
@@ -77,15 +79,21 @@ export function useResizeGhost() {
ghostEl.style.top = `${top}px`;
if (height !== null)
ghostEl.style.height = `${height}px`;
if (width !== null)
ghostEl.style.width = `${width}px`;
if (left !== null)
ghostEl.style.left = `${left}px`;
}
function getPosition()
{
if (!ghostEl)
return { top: 0, height: 0 };
return { top: 0, height: 0, left: 0, width: 0 };
return {
top: parseFloat(ghostEl.style.top),
height: parseFloat(ghostEl.style.height)
height: parseFloat(ghostEl.style.height),
left: parseFloat(ghostEl.style.left),
width: parseFloat(ghostEl.style.width)
};
}
@@ -99,4 +107,4 @@ export function useResizeGhost() {
}
return { create, updateLabel, updatePosition, getPosition, remove };
}
}
+66 -23
View File
@@ -41,20 +41,30 @@ function getSnapTimes(timeGrid, dayISO, zoneName)
function calculateNewTimes(activeResize, ghostPosition)
{
const { edge, event, timeGrid, startTop, startHeight } = activeResize;
const { edge, event, timeGrid, horizontal, startTop, startHeight, startLeft, startWidth } = activeResize;
const { start, end } = event;
const durationMinutes = end.diff(start, 'minutes').minutes;
if (!durationMinutes || durationMinutes <= 0)
return null;
const pxPerMinute = startHeight / durationMinutes;
const startSize = horizontal ? startWidth : startHeight;
if (!Number.isFinite(startSize) || startSize <= 0)
return null;
const pxPerMinute = startSize / durationMinutes;
let draggedPx = 0;
if (edge === 'end')
draggedPx = ghostPosition.height - startHeight;
draggedPx = horizontal
? ghostPosition.width - startWidth
: ghostPosition.height - startHeight;
if (edge === 'start')
draggedPx = ghostPosition.top - startTop;
draggedPx = horizontal
? ghostPosition.left - startLeft
: ghostPosition.top - startTop;
if (!Number.isFinite(draggedPx))
return null;
const draggedMinutes = snapToGrid(draggedPx / pxPerMinute, edge);
@@ -97,6 +107,12 @@ export function useResizeHandler() {
return (evt.clientY - gridRect.top) + activeResize.gridEl.scrollTop;
}
function getPointerXInGrid(evt)
{
const gridRect = activeResize.gridEl.getBoundingClientRect();
return (evt.clientX - gridRect.left) + activeResize.gridEl.scrollLeft;
}
function updateGhostLabel()
{
const result = calculateNewTimes(activeResize, ghost.getPosition());
@@ -111,36 +127,59 @@ export function useResizeHandler() {
return;
evt.preventDefault();
const maxBottom = activeResize.gridEl.scrollHeight;
const pointerY = getPointerYInGrid(evt);
const draggedPx = pointerY - activeResize.dragStartY;
const maxEnd = activeResize.horizontal
? activeResize.gridEl.scrollWidth
: activeResize.gridEl.scrollHeight;
const pointer = activeResize.horizontal
? getPointerXInGrid(evt)
: getPointerYInGrid(evt);
const draggedPx = pointer - (activeResize.horizontal
? activeResize.dragStartX
: activeResize.dragStartY);
if (activeResize.edge === 'end')
{
let newHeight = Math.max(MIN_HEIGHT_PX, activeResize.startHeight + draggedPx);
if (activeResize.startTop + newHeight > maxBottom)
newHeight = maxBottom - activeResize.startTop;
const minSize = MIN_HEIGHT_PX;
const newSize = Math.max(minSize, (activeResize.horizontal
? activeResize.startWidth
: activeResize.startHeight) + draggedPx);
const startPosition = activeResize.horizontal
? activeResize.startLeft
: activeResize.startTop;
const constrainedSize = Math.min(newSize, maxEnd - startPosition);
ghost.updatePosition(null, newHeight);
if (activeResize.horizontal)
ghost.updatePosition(null, null, constrainedSize);
else
ghost.updatePosition(null, constrainedSize);
}
else if (activeResize.edge === 'start')
{
let newTop = activeResize.startTop + draggedPx;
let newHeight = activeResize.startHeight - draggedPx;
const startPosition = activeResize.horizontal
? activeResize.startLeft
: activeResize.startTop;
const startSize = activeResize.horizontal
? activeResize.startWidth
: activeResize.startHeight;
let newPosition = startPosition + draggedPx;
let newSize = startSize - draggedPx;
if (newTop < 0)
if (newPosition < 0)
{
newHeight -= (0 - newTop);
newTop = 0;
newSize -= (0 - newPosition);
newPosition = 0;
}
if (newHeight < MIN_HEIGHT_PX)
if (newSize < MIN_HEIGHT_PX)
{
newTop = (activeResize.startTop + activeResize.startHeight) - MIN_HEIGHT_PX;
newHeight = MIN_HEIGHT_PX;
newPosition = (startPosition + startSize) - MIN_HEIGHT_PX;
newSize = MIN_HEIGHT_PX;
}
ghost.updatePosition(newTop, newHeight);
if (activeResize.horizontal)
ghost.updatePosition(null, null, newSize, newPosition);
else
ghost.updatePosition(newPosition, newSize);
}
updateGhostLabel();
@@ -176,9 +215,9 @@ export function useResizeHandler() {
activeResize = null;
}
function startResize(edge, evt, { el, gridEl, event, timeGrid, onEnd })
function startResize(edge, evt, { el, gridEl, event, horizontal = false, timeGrid, onEnd })
{
const { startTop, startHeight } = ghost.create(gridEl, el, edge);
const { startTop, startHeight, startLeft, startWidth } = ghost.create(gridEl, el, edge);
activeResize = {
edge,
@@ -186,11 +225,15 @@ export function useResizeHandler() {
eventEl: el,
gridEl,
event,
horizontal,
timeGrid,
onEnd,
dragStartY: (evt.clientY - gridEl.getBoundingClientRect().top) + gridEl.scrollTop,
dragStartX: (evt.clientX - gridEl.getBoundingClientRect().left) + gridEl.scrollLeft,
startTop,
startHeight,
startLeft,
startWidth,
originalOpacity: el.style.opacity,
};
@@ -214,4 +257,4 @@ export function useResizeHandler() {
}
return { startResize, cleanup };
}
}