diff --git a/common/helpers/__tests__/interpolation.spec.js b/common/helpers/__tests__/interpolation.spec.js new file mode 100644 index 0000000000..71a20bc76c --- /dev/null +++ b/common/helpers/__tests__/interpolation.spec.js @@ -0,0 +1,65 @@ +import { lerp, clamp, invlerp, range } from '../interpolation.js' + +describe('lerp', () => { + it.each([ + [[0, 50, 0.5], 25], + [[1, 3, 0.5], 2], + [[0, 5, 0.2], 1], + [[2, 7, 1], 7], + [[8, 4, 0.5], 6], + [[5, 4, 1], 4], + [[3, 7, 2], 11], + [[2, 3, 3], 5], + [[-8, -4, 0.5], -6], + [[-2, -4, 0.25], -2.5], + [[0, 10, 0.1], 1], + [[10, 50, 1], 50], + ])('maps %p to %p', (input, expected) => { + expect(lerp(...input)).toEqual(expected) + }) +}) + +describe('clamp', () => { + it.each([ + [[0.5, 0, 50], 0.5], + [[3, 0, 5], 3], + [[2, -5, 10], 2], + [[8, 1, 3], 3], + [[5, -1, 2], 2], + [[3, 5, 10], 5], + [[1, 2, 3], 2], + [[11, 0, 10], 10], + [[1, 10, 50], 10], + ])('maps %p to %p', (input, expected) => { + expect(clamp(...input)).toEqual(expected) + }) +}) + +describe('invlerp', () => { + it.each([ + [[0, 2, 1], 0.5], + [[-10, 0, -5], 0.5], + [[-10, 10, 8], 0.9], + [[3, 7, 5], 0.5], + [[-1, 1, 10], 1], + [[99, 101, 42], 0], + [[-100, 100, -100], 0], + ])('maps %p to %p', (input, expected) => { + expect(invlerp(...input)).toEqual(expected) + }) +}) + +describe('range', () => { + it.each([ + [[0, 1, 10, 20, 0.5], 15], + [[10, 0, 20, 40, 7.5], 25], + [[-10, 10, 8, 96, 5], 74], + [[16, 32, 8, 14, 24], 11], + [[-100, 100, 0, 100, 0], 50], + [[-100, 100, 0, 100, 50], 75], + [[42, 42, 0, 100, 42], NaN], + [[1337, 50, 0, 100, 42], 100], + ])('maps %p to %p', (input, expected) => { + expect(range(...input)).toEqual(expected) + }) +}) diff --git a/common/helpers/interpolation.js b/common/helpers/interpolation.js new file mode 100644 index 0000000000..5ea4f7bb09 --- /dev/null +++ b/common/helpers/interpolation.js @@ -0,0 +1,39 @@ +/** + * Curve fitting + * @param x + * @param y + * @param a + * @returns {number} + */ +export const lerp = (x, y, a) => x * (1 - a) + y * a + +/** + * Clamps value between min and max + * @param a + * @param min + * @param max + * @returns {number} + */ +export const clamp = (a, min = 0, max = 1) => Math.min(max, Math.max(min, a)) + +/** + * Inverse curve fitting function + * @param x + * @param y + * @param a + * @returns {number} + */ +export const invlerp = (x, y, a) => clamp((a - x) / (y - x)) + +/** + * Maps input to output range + * @param inputStart + * @param inputEnd + * @param outputStart + * @param outputEnd + * @param input + * @returns number + */ +export function range(inputStart, inputEnd, outputStart, outputEnd, input) { + return lerp(outputStart, outputEnd, invlerp(inputStart, inputEnd, input)) +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue index ed9027f5f7..5328f1db8a 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -10,7 +10,7 @@ - + eCamp diff --git a/frontend/src/components/activity/ActivityResponsibles.vue b/frontend/src/components/activity/ActivityResponsibles.vue index 93f0194726..8a297b2f0f 100644 --- a/frontend/src/components/activity/ActivityResponsibles.vue +++ b/frontend/src/components/activity/ActivityResponsibles.vue @@ -14,16 +14,30 @@ small-chips v-bind="$attrs" @input="onInput" - /> + > + + + + diff --git a/frontend/src/components/dashboard/ActivityRow.vue b/frontend/src/components/dashboard/ActivityRow.vue index b936cca699..9095134c73 100644 --- a/frontend/src/components/dashboard/ActivityRow.vue +++ b/frontend/src/components/dashboard/ActivityRow.vue @@ -17,20 +17,20 @@ {{ title }}
{{ location }} - + diff --git a/frontend/src/components/program/ScheduleEntries.vue b/frontend/src/components/program/ScheduleEntries.vue index 546ca3d414..5ff011af82 100644 --- a/frontend/src/components/program/ScheduleEntries.vue +++ b/frontend/src/components/program/ScheduleEntries.vue @@ -10,8 +10,8 @@ diff --git a/frontend/src/components/program/picasso/PicassoEntry.vue b/frontend/src/components/program/picasso/PicassoEntry.vue new file mode 100644 index 0000000000..708a8a4bc0 --- /dev/null +++ b/frontend/src/components/program/picasso/PicassoEntry.vue @@ -0,0 +1,369 @@ + + + + diff --git a/frontend/src/components/program/picasso/useClickDetector.js b/frontend/src/components/program/picasso/useClickDetector.js index dd5463d274..ec25a21f00 100644 --- a/frontend/src/components/program/picasso/useClickDetector.js +++ b/frontend/src/components/program/picasso/useClickDetector.js @@ -1,10 +1,11 @@ /** * - * @param ref(bool) enabled false disables click detection - * @param int threshold max. mouse movement to still detect as a click + * @param enabled {Ref} false disables click detection + * @param threshold {number} max. mouse movement to still detect as a click + * @param onClick {() => void} run function on click * @returns */ -export default function useClickDetector(enabled = true, threshold = 5, onClick = null) { +export function useClickDetector(enabled, threshold = 5, onClick = null) { /** * internal data (not exposed) */ @@ -32,7 +33,7 @@ export default function useClickDetector(enabled = true, threshold = 5, onClick /** * exposed methods */ - const entryMouseDown = ({ nativeEvent }) => { + const entryMouseDown = (nativeEvent) => { if (!enabled.value) { return } @@ -41,7 +42,7 @@ export default function useClickDetector(enabled = true, threshold = 5, onClick startY = nativeEvent.y } - const entryMouseMove = ({ nativeEvent }) => { + const entryMouseMove = (nativeEvent) => { if (startX === null) { return } @@ -52,7 +53,7 @@ export default function useClickDetector(enabled = true, threshold = 5, onClick } } - const entryMouseUp = ({ event, nativeEvent }) => { + const entryMouseUp = (nativeEvent) => { if (startX === null) { return } @@ -75,7 +76,7 @@ export default function useClickDetector(enabled = true, threshold = 5, onClick // onClick callback if (onClick !== null) { - onClick(event) + onClick() } } @@ -83,10 +84,10 @@ export default function useClickDetector(enabled = true, threshold = 5, onClick } return { - vCalendarListeners: { - 'mousedown:event': entryMouseDown, - 'mousemove:event': entryMouseMove, - 'mouseup:event': entryMouseUp, + listeners: { + mousedown: entryMouseDown, + mousemove: entryMouseMove, + mouseup: entryMouseUp, }, } } diff --git a/frontend/src/components/program/picasso/useDragAndDropMove.js b/frontend/src/components/program/picasso/useDragAndDropMove.js index a97d02a126..28bf7c4063 100644 --- a/frontend/src/components/program/picasso/useDragAndDropMove.js +++ b/frontend/src/components/program/picasso/useDragAndDropMove.js @@ -1,15 +1,14 @@ import { toTime, roundTimeDown } from '@/helpers/vCalendarDragAndDrop.js' /** - * - * @param ref(bool) enabled drag & drop is disabled if enabled=false - * @param int threshold min. mouse movement needed to detect drag & drop - * @param update function callback for update actions - * @param minTimestamp minimum allowed start timestamp (calendar start) - * @param maxTimestamp maximum allowed end timestamp (calender end) + * @param enabled {Ref} drag & drop is disabled if enabled=false + * @param threshold {number} min. mouse movement needed to detect drag & drop + * @param update {(scheduleEntry: object, startTime: number, endTime: number) => void} callback for update actions + * @param minTimestamp {number} minimum allowed start timestamp (calendar start) + * @param maxTimestamp {number} maximum allowed end timestamp (calendar end) * @returns */ -export default function useDragAndDrop( +export function useDragAndDropMove( enabled, threshold, update, diff --git a/frontend/src/components/program/picasso/useDragAndDropNew.js b/frontend/src/components/program/picasso/useDragAndDropNew.js index de9c400b05..e102cc9514 100644 --- a/frontend/src/components/program/picasso/useDragAndDropNew.js +++ b/frontend/src/components/program/picasso/useDragAndDropNew.js @@ -1,12 +1,12 @@ -import { toTime, roundTimeUp, roundTimeDown } from '@/helpers/vCalendarDragAndDrop.js' +import { toTime, roundTimeDown, minMaxTime } from '@/helpers/vCalendarDragAndDrop.js' /** * - * @param ref(bool) enabled drag & drop is disabled if enabled=false - * @param int threshold min. mouse movement needed to detect drag & drop + * @param enabled {Ref} drag & drop is disabled if enabled=false + * @param createEntry {(startTime:number, endTime:number, finished:boolean) => void} * @returns */ -export default function useDragAndDrop(enabled, createEntry) { +export function useDragAndDropNew(enabled, createEntry) { /** * internal data (not exposed) */ @@ -40,9 +40,7 @@ export default function useDragAndDrop(enabled, createEntry) { // resize placeholder entry const resizeEntry = (entry, mouse) => { - const mouseRounded = roundTimeUp(mouse) - const min = Math.min(mouseRounded, roundTimeDown(mouseStartTimestamp)) - const max = Math.max(mouseRounded, roundTimeDown(mouseStartTimestamp)) + const { min, max } = minMaxTime(mouse, mouseStartTimestamp) entry.startTimestamp = min entry.endTimestamp = max diff --git a/frontend/src/components/program/picasso/useDragAndDropReminder.js b/frontend/src/components/program/picasso/useDragAndDropReminder.js new file mode 100644 index 0000000000..fe57aba1a8 --- /dev/null +++ b/frontend/src/components/program/picasso/useDragAndDropReminder.js @@ -0,0 +1,94 @@ +import { toTime, minMaxTime, ONE_HOUR } from '@/helpers/vCalendarDragAndDrop.js' + +/** + * + * @param enabled {Ref} drag & drop is disabled if enabled=false + * @param showReminder {(move?: boolean) => void} function to show the reminder + * @returns + */ +export function useDragAndDropReminder(enabled, showReminder) { + /** + * internal data (not exposed) + */ + + // temporary placeholder for new schedule entry, when created via drag & drop + let mouseStartTimestamp = null + + // true if mousedown event was detected on an entry/event + let entryWasClicked = false + + /** + * internal methods + */ + + const clear = () => { + mouseStartTimestamp = null + entryWasClicked = false + } + + /** + * exposed methods + */ + + // triggered with MouseDown event on a calendar entry + const entryMouseDown = ({ event: entry, timed, nativeEvent }) => { + if (enabled.value) { + return + } + + // cancel drag & drop if button is not left button + if (nativeEvent.button !== 0) { + return + } + + if (entry && timed) { + entryWasClicked = true + } + } + + // triggered with MouseDown event anywhere on the calendar (independent of clicking on entry or not) + const timeMouseDown = (tms, nativeEvent) => { + if (enabled.value) { + return + } + + // cancel drag & drop if button is not left button + if (nativeEvent.button !== 0) { + return + } + + mouseStartTimestamp = toTime(tms) + } + + // triggered when mouse is being moved in calendar (independent whether drag & drop is ongoing or not) + const timeMouseMove = (tms) => { + if (enabled.value || mouseStartTimestamp == null) { + return + } + + const { min, max } = minMaxTime(mouseStartTimestamp, toTime(tms)) + + if (max - min >= ONE_HOUR) { + showReminder(entryWasClicked) + } + } + + // triggered with MouseUp Event anywhere in the calendar + const timeMouseUp = () => { + if (enabled.value) { + return + } + + clear() + } + + return { + vCalendarListeners: { + 'mousedown:event': entryMouseDown, + 'mousedown:time': timeMouseDown, + 'mousemove:time': timeMouseMove, + 'mouseup:time': timeMouseUp, + }, + nativeMouseLeave: clear, + } +} diff --git a/frontend/src/components/program/picasso/useDragAndDropResize.js b/frontend/src/components/program/picasso/useDragAndDropResize.js index 2abca2d648..9f4e319f7d 100644 --- a/frontend/src/components/program/picasso/useDragAndDropResize.js +++ b/frontend/src/components/program/picasso/useDragAndDropResize.js @@ -1,14 +1,14 @@ -import { toTime, roundTimeUp, roundTimeDown } from '@/helpers/vCalendarDragAndDrop.js' +import { toTime, minMaxTime } from '@/helpers/vCalendarDragAndDrop.js' /** * - * @param ref(bool) enabled drag & drop is disabled if enabled=false - * @param int threshold min. mouse movement needed to detect drag & drop - * @param minTimestamp minimum allowed start timestamp (calendar start) - * @param maxTimestamp maximum allowed end timestamp (calender end) + * @param enabled {Ref} drag & drop is disabled if enabled=false + * @param update {(scheduleEntry: object, startTime: number, endTime: number) => void} callback for update actions + * @param minTimestamp {number} minimum allowed start timestamp (calendar start) + * @param maxTimestamp {number} maximum allowed end timestamp (calendar end) * @returns */ -export default function useDragAndDrop(enabled, update, minTimestamp, maxTimestamp) { +export function useDragAndDropResize(enabled, update, minTimestamp, maxTimestamp) { /** * internal data (not exposed) */ @@ -28,9 +28,7 @@ export default function useDragAndDrop(enabled, update, minTimestamp, maxTimesta // resize an entry (existing or new placeholder) const resizeEntry = (entry, mouse) => { - const mouseRounded = roundTimeUp(mouse) - const newStart = Math.min(mouseRounded, roundTimeDown(originalStartTimestamp)) - const newEnd = Math.max(mouseRounded, roundTimeDown(originalStartTimestamp)) + const { min: newStart, max: newEnd } = minMaxTime(originalStartTimestamp, mouse) if (newStart >= minTimestamp && newEnd <= maxTimestamp && newEnd - newStart > 0) { // TODO review: Here we're changing the store value directly. diff --git a/frontend/src/components/user/UserAvatar.vue b/frontend/src/components/user/UserAvatar.vue index 8b2259f75d..246cf2df0f 100644 --- a/frontend/src/components/user/UserAvatar.vue +++ b/frontend/src/components/user/UserAvatar.vue @@ -1,5 +1,5 @@ + + mdi-lock + {{ reminderText }} + diff --git a/frontend/src/views/camp/Dashboard.vue b/frontend/src/views/camp/Dashboard.vue index 11784ec52b..536ec29693 100644 --- a/frontend/src/views/camp/Dashboard.vue +++ b/frontend/src/views/camp/Dashboard.vue @@ -19,11 +19,12 @@ :label="$tc('views.camp.dashboard.responsible')" > @@ -182,6 +183,7 @@ import FilterDivider from '@/components/dashboard/FilterDivider.vue' import { keyBy, groupBy, mapValues } from 'lodash' import campCollaborationDisplayName from '../../common/helpers/campCollaborationDisplayName.js' import { dateHelperUTCFormatted } from '@/mixins/dateHelperUTCFormatted.js' +import TextAlignBaseline from '@/components/layout/TextAlignBaseline.vue' function filterEquals(arr1, arr2) { return JSON.stringify(arr1) === JSON.stringify(arr2) @@ -190,6 +192,7 @@ function filterEquals(arr1, arr2) { export default { name: 'Dashboard', components: { + TextAlignBaseline, FilterDivider, ActivityRow, SelectFilter, diff --git a/frontend/src/views/camp/navigation/NavigationCamp.vue b/frontend/src/views/camp/navigation/NavigationCamp.vue index bd15178105..16112f7a16 100644 --- a/frontend/src/views/camp/navigation/NavigationCamp.vue +++ b/frontend/src/views/camp/navigation/NavigationCamp.vue @@ -1,5 +1,5 @@