mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-22 16:27:22 +00:00
Calendar: resize and move event (#3750)
Signed-off-by: Alexander Platov <sas_lord@mail.ru>
This commit is contained in:
parent
75b71e9b1a
commit
a3fd97e3b4
@ -793,6 +793,7 @@ a.no-line {
|
|||||||
.cursor-pointer { cursor: pointer; }
|
.cursor-pointer { cursor: pointer; }
|
||||||
.cursor-default { cursor: default; }
|
.cursor-default { cursor: default; }
|
||||||
.cursor-inherit { cursor: inherit; }
|
.cursor-inherit { cursor: inherit; }
|
||||||
|
.cursor-row-resize { cursor: row-resize; }
|
||||||
|
|
||||||
.pointer-events-none { pointer-events: none; }
|
.pointer-events-none { pointer-events: none; }
|
||||||
.content-pointer-events-none > * { pointer-events: none; }
|
.content-pointer-events-none > * { pointer-events: none; }
|
||||||
|
@ -13,8 +13,9 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Event } from '@hcengineering/calendar'
|
import { Event, ReccuringInstance } from '@hcengineering/calendar'
|
||||||
import { Timestamp } from '@hcengineering/core'
|
import { Timestamp, Ref, DocumentUpdate } from '@hcengineering/core'
|
||||||
|
import { getClient } from '@hcengineering/presentation'
|
||||||
import ui, {
|
import ui, {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
CalendarItem,
|
CalendarItem,
|
||||||
@ -25,14 +26,19 @@
|
|||||||
Scroller,
|
Scroller,
|
||||||
addZero,
|
addZero,
|
||||||
areDatesEqual,
|
areDatesEqual,
|
||||||
|
closeTooltip,
|
||||||
deviceOptionsStore as deviceInfo,
|
deviceOptionsStore as deviceInfo,
|
||||||
day as getDay,
|
day as getDay,
|
||||||
getMonday,
|
getMonday,
|
||||||
getWeekDayName,
|
getWeekDayName,
|
||||||
resizeObserver
|
resizeObserver,
|
||||||
|
showPopup,
|
||||||
|
getEventPositionElement
|
||||||
} from '@hcengineering/ui'
|
} from '@hcengineering/ui'
|
||||||
|
import { Menu } from '@hcengineering/view-resources'
|
||||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||||
import calendar from '../plugin'
|
import calendar from '../plugin'
|
||||||
|
import { updateReccuringInstance, isReadOnly } from '../utils'
|
||||||
import EventElement from './EventElement.svelte'
|
import EventElement from './EventElement.svelte'
|
||||||
|
|
||||||
export let events: Event[]
|
export let events: Event[]
|
||||||
@ -47,6 +53,7 @@
|
|||||||
export let showHeader: boolean = true
|
export let showHeader: boolean = true
|
||||||
export let clearCells: boolean = false
|
export let clearCells: boolean = false
|
||||||
|
|
||||||
|
const client = getClient()
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
const todayDate = new Date()
|
const todayDate = new Date()
|
||||||
@ -132,6 +139,16 @@
|
|||||||
dueDate: Timestamp
|
dueDate: Timestamp
|
||||||
cols: number
|
cols: number
|
||||||
}
|
}
|
||||||
|
interface CalendarElementRect {
|
||||||
|
top: number
|
||||||
|
bottom: number
|
||||||
|
left: number
|
||||||
|
right: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
fit: boolean
|
||||||
|
visibility: number
|
||||||
|
}
|
||||||
interface CalendarColumn {
|
interface CalendarColumn {
|
||||||
elements: CalendarElement[]
|
elements: CalendarElement[]
|
||||||
}
|
}
|
||||||
@ -169,6 +186,7 @@
|
|||||||
let adMaxRow: number = 1
|
let adMaxRow: number = 1
|
||||||
let adRows: CalendarADRows[]
|
let adRows: CalendarADRows[]
|
||||||
const cellBorder: number = 1
|
const cellBorder: number = 1
|
||||||
|
const stepsPerHour: number = 4
|
||||||
const heightAD: number = 2
|
const heightAD: number = 2
|
||||||
const minAD: number = 2
|
const minAD: number = 2
|
||||||
const maxAD: number = 3
|
const maxAD: number = 3
|
||||||
@ -178,6 +196,16 @@
|
|||||||
let shownAD: boolean = false
|
let shownAD: boolean = false
|
||||||
let shortAlldays: { id: string; day: number; fixRow?: boolean }[] = []
|
let shortAlldays: { id: string; day: number; fixRow?: boolean }[] = []
|
||||||
let moreCounts: number[] = Array<number>(displayedDaysCount)
|
let moreCounts: number[] = Array<number>(displayedDaysCount)
|
||||||
|
const nullCalendarElement: CalendarElementRect = {
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
width: 0,
|
||||||
|
height: rem(heightAD),
|
||||||
|
fit: true,
|
||||||
|
visibility: 1
|
||||||
|
}
|
||||||
|
|
||||||
$: if (newEvents !== calendarEvents) {
|
$: if (newEvents !== calendarEvents) {
|
||||||
newEvents = calendarEvents
|
newEvents = calendarEvents
|
||||||
@ -340,14 +368,12 @@
|
|||||||
return mins < 2 ? (end ? 1 : 2) : mins >= 57 ? (end ? 1 + cellBorder : 1) : 1
|
return mins < 2 ? (end ? 1 : 2) : mins >= 57 ? (end ? 1 + cellBorder : 1) : 1
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRect = (
|
const getRect = (event: CalendarItem): CalendarElementRect => {
|
||||||
event: CalendarItem
|
const result = { ...nullCalendarElement }
|
||||||
): { top: number; bottom: number; left: number; right: number; width: number; visibility: number } => {
|
|
||||||
const result = { top: 0, bottom: 0, left: 0, right: 0, width: 0, visibility: 1 }
|
|
||||||
const checkDate = new Date(weekMonday.getTime() + MILLISECONDS_IN_DAY * event.day)
|
const checkDate = new Date(weekMonday.getTime() + MILLISECONDS_IN_DAY * event.day)
|
||||||
const startDay = checkDate.setHours(startHour, 0, 0, 0)
|
const startDay = checkDate.setHours(startHour, 0, 0, 0)
|
||||||
const endDay = checkDate.setHours(displayedHours - 1, 59, 59, 999)
|
const endDay = checkDate.setHours(displayedHours - 1, 59, 59, 999)
|
||||||
const startTime = event.date < startDay ? { hours: 0, mins: 0 } : convertToTime(event.date)
|
const startTime = event.date <= startDay ? { hours: startHour, mins: 0 } : convertToTime(event.date)
|
||||||
const endTime =
|
const endTime =
|
||||||
event.dueDate > endDay ? { hours: displayedHours - startHour, mins: 0 } : convertToTime(event.dueDate)
|
event.dueDate > endDay ? { hours: displayedHours - startHour, mins: 0 } : convertToTime(event.dueDate)
|
||||||
if (getDay(weekMonday, event.day).setHours(endTime.hours, endTime.mins, 0, 0) <= todayDate.getTime()) {
|
if (getDay(weekMonday, event.day).setHours(endTime.hours, endTime.mins, 0, 0) <= todayDate.getTime()) {
|
||||||
@ -383,12 +409,8 @@
|
|||||||
(cols - index - 1) * rem(0.125)
|
(cols - index - 1) * rem(0.125)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
const getADRect = (
|
const getADRect = (id: string, day?: number, fixRow?: boolean): CalendarElementRect => {
|
||||||
id: string,
|
const result = { ...nullCalendarElement }
|
||||||
day?: number,
|
|
||||||
fixRow?: boolean
|
|
||||||
): { top: number; left: number; width: number; height: number; fit: boolean; visibility: number } => {
|
|
||||||
const result = { top: 0, left: 0, width: 0, height: rem(heightAD), fit: true, visibility: 1 }
|
|
||||||
const index = adRows.findIndex((ev) => ev.id === id)
|
const index = adRows.findIndex((ev) => ev.id === id)
|
||||||
|
|
||||||
const checkTime = new Date().setHours(0, 0, 0, 0)
|
const checkTime = new Date().setHours(0, 0, 0, 0)
|
||||||
@ -516,10 +538,116 @@
|
|||||||
: rem((heightAD + 0.125) * (adMaxRow <= maxAD ? adMaxRow : maxAD) + 0.25)
|
: rem((heightAD + 0.125) * (adMaxRow <= maxAD ? adMaxRow : maxAD) + 0.25)
|
||||||
$: showArrowAD = (!minimizedAD && adMaxRow > maxAD) || (minimizedAD && adMaxRow > minAD)
|
$: showArrowAD = (!minimizedAD && adMaxRow > maxAD) || (minimizedAD && adMaxRow > minAD)
|
||||||
|
|
||||||
let dragOnOld: CalendarCell | null = null
|
const getMinutes = (e: MouseEvent): number => {
|
||||||
|
let mins: number = 0
|
||||||
|
for (let i = 0; i < stepsPerHour; i++) {
|
||||||
|
if (e.offsetY >= (i * cellHeight) / stepsPerHour && e.offsetY < ((i + 1) * cellHeight) / stepsPerHour) {
|
||||||
|
mins = (i * 60) / stepsPerHour
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mins
|
||||||
|
}
|
||||||
|
|
||||||
const getMinutes = (e: MouseEvent): number => (e.offsetY >= cellHeight / 2 ? 30 : 0)
|
let dragOnOld: CalendarCell | null = null
|
||||||
const dragOver = (e: DragEvent & { currentTarget: EventTarget & HTMLDivElement }, day: Date, hourOfDay: number) => {
|
let dragId: Ref<Event> | null = null
|
||||||
|
let resizeId: Ref<Event> | null = null
|
||||||
|
let directionResize: 'top' | 'bottom' | null
|
||||||
|
let oldMins: number = 0
|
||||||
|
let oldTime: number = -1
|
||||||
|
let originDate: Timestamp = 0
|
||||||
|
let originDueDate: Timestamp = 0
|
||||||
|
|
||||||
|
async function updateHandler (event: Event) {
|
||||||
|
const update: DocumentUpdate<Event> = {}
|
||||||
|
if (originDate !== event.date) update.date = event.date
|
||||||
|
if (originDueDate !== event.dueDate) update.dueDate = event.dueDate
|
||||||
|
if (Object.keys(update).length > 0) {
|
||||||
|
if (event._class === calendar.class.ReccuringInstance) {
|
||||||
|
await updateReccuringInstance(update, event as ReccuringInstance)
|
||||||
|
} else {
|
||||||
|
await client.update(event, update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function mouseUpElement (e: MouseEvent) {
|
||||||
|
window.removeEventListener('mouseup', mouseUpElement)
|
||||||
|
const event = events.find((ev) => ev._id === resizeId)
|
||||||
|
if (event !== undefined) await updateHandler(event)
|
||||||
|
resizeId = directionResize = null
|
||||||
|
}
|
||||||
|
function mouseDownElement (e: MouseEvent, event: Event, direction: 'top' | 'bottom'): void {
|
||||||
|
if (e.buttons !== 1) return
|
||||||
|
e.stopPropagation()
|
||||||
|
resizeId = event._id
|
||||||
|
directionResize = direction
|
||||||
|
originDate = event.date
|
||||||
|
originDueDate = event.dueDate
|
||||||
|
window.addEventListener('mouseup', mouseUpElement)
|
||||||
|
}
|
||||||
|
function mouseMoveElement (
|
||||||
|
e: MouseEvent & { currentTarget: EventTarget & HTMLDivElement },
|
||||||
|
day: Date,
|
||||||
|
hour: number
|
||||||
|
): void {
|
||||||
|
if (resizeId == null && directionResize == null) return
|
||||||
|
let mins: number = 0
|
||||||
|
for (let i = 0; i < stepsPerHour; i++) {
|
||||||
|
if (e.offsetY >= (i * cellHeight) / stepsPerHour && e.offsetY < ((i + 1) * cellHeight) / stepsPerHour) mins = i
|
||||||
|
}
|
||||||
|
if (oldMins === mins) return
|
||||||
|
oldMins = mins
|
||||||
|
const newDate = new Date(day).setHours(
|
||||||
|
hour + startHour,
|
||||||
|
(mins * 60) / stepsPerHour + (directionResize === 'top' ? 0 : 60 / stepsPerHour),
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
const index = events.findIndex((ev) => ev._id === resizeId)
|
||||||
|
if (index === -1) return
|
||||||
|
if (directionResize === 'top') {
|
||||||
|
if (events[index].dueDate - newDate >= 15 * 60000) events[index].date = newDate
|
||||||
|
} else {
|
||||||
|
if (newDate - events[index].date >= 15 * 60000) events[index].dueDate = newDate
|
||||||
|
}
|
||||||
|
events = events
|
||||||
|
}
|
||||||
|
function dragStartElement (e: DragEvent & { currentTarget: EventTarget & HTMLDivElement }, event: Event): void {
|
||||||
|
if (isReadOnly(event) || event.allDay) return
|
||||||
|
if (e.dataTransfer) e.dataTransfer.effectAllowed = 'all'
|
||||||
|
originDate = event.date
|
||||||
|
originDueDate = event.dueDate
|
||||||
|
dragOnOld = null
|
||||||
|
oldTime = -1
|
||||||
|
closeTooltip()
|
||||||
|
setTimeout(() => (dragId = event._id), 50)
|
||||||
|
}
|
||||||
|
async function dragEndElement (e: DragEvent) {
|
||||||
|
const event = events.find((ev) => ev._id === dragId)
|
||||||
|
if (event !== undefined) await updateHandler(event)
|
||||||
|
dragId = null
|
||||||
|
}
|
||||||
|
function dragDrop (e: DragEvent, day: Date, hourOfDay: number): void {
|
||||||
|
const newTime = new Date(day).setHours(hourOfDay + startHour, getMinutes(e), 0, 0)
|
||||||
|
if (dragId) {
|
||||||
|
const index = events.findIndex((ev) => ev._id === dragId)
|
||||||
|
const diff = newTime - oldTime
|
||||||
|
if (diff && index !== -1 && oldTime !== -1) {
|
||||||
|
events[index].date = originDate + diff
|
||||||
|
events[index].dueDate = originDueDate + diff
|
||||||
|
events = events
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dispatch('dragDrop', {
|
||||||
|
day,
|
||||||
|
hour: hourOfDay + startHour,
|
||||||
|
date: new Date(newTime)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
dragOnOld = null
|
||||||
|
}
|
||||||
|
function dragOver (e: DragEvent, day: Date, hourOfDay: number): void {
|
||||||
|
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'
|
||||||
|
e.preventDefault()
|
||||||
const dragOn: CalendarCell = {
|
const dragOn: CalendarCell = {
|
||||||
day,
|
day,
|
||||||
hourOfDay,
|
hourOfDay,
|
||||||
@ -527,20 +655,31 @@
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
dragOnOld !== null &&
|
dragOnOld !== null &&
|
||||||
dragOn.day === dragOnOld.day &&
|
areDatesEqual(dragOn.day, dragOnOld.day) &&
|
||||||
dragOn.hourOfDay === dragOnOld.hourOfDay &&
|
dragOn.hourOfDay === dragOnOld.hourOfDay &&
|
||||||
dragOn.minutes === dragOnOld.minutes
|
dragOn.minutes === dragOnOld.minutes
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
dragOnOld = dragOn
|
dragOnOld = dragOn
|
||||||
dispatch('dragenter', {
|
const newTime = new Date(day).setHours(hourOfDay + startHour, dragOn.minutes, 0, 0)
|
||||||
date: new Date(day.setHours(hourOfDay + startHour, dragOn.minutes, 0, 0))
|
if (dragId) {
|
||||||
})
|
if (oldTime === -1) oldTime = newTime
|
||||||
e.preventDefault()
|
const index = events.findIndex((ev) => ev._id === dragId)
|
||||||
|
const diff = newTime - oldTime
|
||||||
|
if (diff && index !== -1) {
|
||||||
|
events[index].date = originDate + diff
|
||||||
|
events[index].dueDate = originDueDate + diff
|
||||||
|
}
|
||||||
|
events = events
|
||||||
|
} else dispatch('dragEnter', { date: new Date(newTime) })
|
||||||
}
|
}
|
||||||
|
|
||||||
const dragOn = (e: DragEvent) => e.preventDefault()
|
function showMenu (ev: MouseEvent, event: Event) {
|
||||||
|
ev.preventDefault()
|
||||||
|
closeTooltip()
|
||||||
|
showPopup(Menu, { object: event }, getEventPositionElement(ev))
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Scroller
|
<Scroller
|
||||||
@ -550,9 +689,9 @@
|
|||||||
<div
|
<div
|
||||||
bind:this={container}
|
bind:this={container}
|
||||||
on:dragleave
|
on:dragleave
|
||||||
on:dragover={dragOn}
|
|
||||||
class="calendar-container"
|
class="calendar-container"
|
||||||
class:clearCells
|
class:clearCells={clearCells || resizeId !== null || dragId !== null}
|
||||||
|
class:cursor-row-resize={resizeId !== null && directionResize !== null}
|
||||||
style:--calendar-ad-height={styleAD + 'px'}
|
style:--calendar-ad-height={styleAD + 'px'}
|
||||||
style:grid={`${showHeader ? '[header] 3.5rem ' : ''}[all-day] ${styleAD}px repeat(${
|
style:grid={`${showHeader ? '[header] 3.5rem ' : ''}[all-day] ${styleAD}px repeat(${
|
||||||
(displayedHours - startHour) * 2
|
(displayedHours - startHour) * 2
|
||||||
@ -706,14 +845,9 @@
|
|||||||
style:width={`${colWidth}px`}
|
style:width={`${colWidth}px`}
|
||||||
style:grid-column={`col-start ${dayOfWeek + 1} / ${dayOfWeek + 2}`}
|
style:grid-column={`col-start ${dayOfWeek + 1} / ${dayOfWeek + 2}`}
|
||||||
style:grid-row={`row-start ${hourOfDay * 2 + 1} / row-start ${hourOfDay * 2 + 3}`}
|
style:grid-row={`row-start ${hourOfDay * 2 + 1} / row-start ${hourOfDay * 2 + 3}`}
|
||||||
|
on:mousemove={(e) => mouseMoveElement(e, day, hourOfDay)}
|
||||||
on:dragover={(e) => dragOver(e, day, hourOfDay)}
|
on:dragover={(e) => dragOver(e, day, hourOfDay)}
|
||||||
on:drop|preventDefault={(e) => {
|
on:drop|preventDefault={(e) => dragDrop(e, day, hourOfDay)}
|
||||||
dispatch('drop', {
|
|
||||||
day,
|
|
||||||
hour: hourOfDay + startHour,
|
|
||||||
date: new Date(day.setHours(hourOfDay + startHour, getMinutes(e), 0, 0))
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
on:click|stopPropagation={() => {
|
on:click|stopPropagation={() => {
|
||||||
dispatch('create', {
|
dispatch('create', {
|
||||||
date: new Date(day.setHours(hourOfDay + startHour, 0, 0, 0)),
|
date: new Date(day.setHours(hourOfDay + startHour, 0, 0, 0)),
|
||||||
@ -733,14 +867,31 @@
|
|||||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||||
<div
|
<div
|
||||||
class="calendar-element"
|
class="calendar-element"
|
||||||
|
class:past={rect.visibility === 0}
|
||||||
style:top={`${rect.top}px`}
|
style:top={`${rect.top}px`}
|
||||||
style:bottom={`${rect.bottom}px`}
|
style:bottom={`${rect.bottom}px`}
|
||||||
style:left={`${rect.left}px`}
|
style:left={`${rect.left}px`}
|
||||||
style:right={`${rect.right}px`}
|
style:right={`${rect.right}px`}
|
||||||
style:opacity={rect.visibility === 0 ? 0.4 : 1}
|
|
||||||
style:--mask-image={'none'}
|
style:--mask-image={'none'}
|
||||||
|
draggable={!ev.allDay && !resizeId}
|
||||||
tabindex={1000 + i}
|
tabindex={1000 + i}
|
||||||
|
on:dragstart={(e) => dragStartElement(e, ev)}
|
||||||
|
on:dragend={dragEndElement}
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
class="calendar-element-start"
|
||||||
|
class:allowed={!resizeId && !dragId && !clearCells}
|
||||||
|
class:hovered={resizeId === ev._id && directionResize === 'top'}
|
||||||
|
on:mousedown={(e) => mouseDownElement(e, ev, 'top')}
|
||||||
|
on:contextmenu={(e) => showMenu(e, ev)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="calendar-element-end"
|
||||||
|
class:allowed={!resizeId && !dragId && !clearCells}
|
||||||
|
class:hovered={resizeId === ev._id && directionResize === 'bottom'}
|
||||||
|
on:mousedown={(e) => mouseDownElement(e, ev, 'bottom')}
|
||||||
|
on:contextmenu={(e) => showMenu(e, ev)}
|
||||||
|
/>
|
||||||
<EventElement
|
<EventElement
|
||||||
event={ev}
|
event={ev}
|
||||||
hourHeight={cellHeight}
|
hourHeight={cellHeight}
|
||||||
@ -748,12 +899,6 @@
|
|||||||
width: rect.width,
|
width: rect.width,
|
||||||
height: (calendarRect?.height ?? rect.top + rect.bottom) - rect.top - rect.bottom
|
height: (calendarRect?.height ?? rect.top + rect.bottom) - rect.top - rect.bottom
|
||||||
}}
|
}}
|
||||||
on:drop={(e) => {
|
|
||||||
dispatch('drop', {
|
|
||||||
date: new Date(event.date)
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
on:resize={() => (events = events)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@ -778,7 +923,7 @@
|
|||||||
&::after {
|
&::after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
content: '';
|
content: '';
|
||||||
inset: 0;
|
inset: -1px;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -847,7 +992,61 @@
|
|||||||
&:not(.withPointer) {
|
&:not(.withPointer) {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
&-start,
|
||||||
|
&-end {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 0.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
position: absolute;
|
||||||
|
content: '';
|
||||||
|
left: -0.25rem;
|
||||||
|
right: -0.25rem;
|
||||||
|
height: 1rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
transition-property: opacity, border-width, transform;
|
||||||
|
transition-duration: 0.15s;
|
||||||
|
transition-timing-function: var(--timing-main);
|
||||||
|
transform: scale(0.9);
|
||||||
|
opacity: 0;
|
||||||
|
cursor: row-resize;
|
||||||
|
filter: drop-shadow(0 0 2px var(--primary-edit-border-color));
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
&.allowed::after {
|
||||||
|
pointer-events: all;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
&.allowed:hover::after,
|
||||||
|
&.hovered::after {
|
||||||
|
border-width: 1px;
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-start {
|
||||||
|
top: 0;
|
||||||
|
&::after {
|
||||||
|
top: -0.25rem;
|
||||||
|
border-top-color: var(--primary-edit-border-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-end {
|
||||||
|
bottom: 0;
|
||||||
|
&::after {
|
||||||
|
bottom: -0.25rem;
|
||||||
|
border-bottom-color: var(--primary-edit-border-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
:global(.calendar-element.past .event-container) {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
.sticky-header {
|
.sticky-header {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
background-color: var(--theme-comp-header-color);
|
background-color: var(--theme-comp-header-color);
|
||||||
|
@ -13,22 +13,21 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Event, ReccuringEvent, ReccuringInstance, RecurringRule, generateEventId } from '@hcengineering/calendar'
|
import { Event, ReccuringEvent, ReccuringInstance, RecurringRule } from '@hcengineering/calendar'
|
||||||
import { Person } from '@hcengineering/contact'
|
import { Person } from '@hcengineering/contact'
|
||||||
import { DocumentUpdate, Ref } from '@hcengineering/core'
|
import { DocumentUpdate, Ref } from '@hcengineering/core'
|
||||||
import presentation, { getClient } from '@hcengineering/presentation'
|
import presentation, { getClient } from '@hcengineering/presentation'
|
||||||
import { StyledTextBox } from '@hcengineering/text-editor'
|
import { StyledTextBox } from '@hcengineering/text-editor'
|
||||||
import { Button, DAY, EditBox, Icon, IconClose, closePopup, showPopup } from '@hcengineering/ui'
|
import { Button, EditBox, Icon, IconClose, showPopup } from '@hcengineering/ui'
|
||||||
import { deepEqual } from 'fast-equals'
|
import { deepEqual } from 'fast-equals'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import calendar from '../plugin'
|
import calendar from '../plugin'
|
||||||
import { isReadOnly, saveUTC } from '../utils'
|
import { isReadOnly, saveUTC, updateReccuringInstance } from '../utils'
|
||||||
import EventParticipants from './EventParticipants.svelte'
|
import EventParticipants from './EventParticipants.svelte'
|
||||||
import EventReminders from './EventReminders.svelte'
|
import EventReminders from './EventReminders.svelte'
|
||||||
import EventTimeEditor from './EventTimeEditor.svelte'
|
import EventTimeEditor from './EventTimeEditor.svelte'
|
||||||
import EventTimeExtraButton from './EventTimeExtraButton.svelte'
|
import EventTimeExtraButton from './EventTimeExtraButton.svelte'
|
||||||
import ReccurancePopup from './ReccurancePopup.svelte'
|
import ReccurancePopup from './ReccurancePopup.svelte'
|
||||||
import UpdateRecInstancePopup from './UpdateRecInstancePopup.svelte'
|
|
||||||
|
|
||||||
export let object: Event
|
export let object: Event
|
||||||
$: readOnly = isReadOnly(object)
|
$: readOnly = isReadOnly(object)
|
||||||
@ -96,7 +95,7 @@
|
|||||||
|
|
||||||
if (Object.keys(update).length > 0) {
|
if (Object.keys(update).length > 0) {
|
||||||
if (object._class === calendar.class.ReccuringInstance) {
|
if (object._class === calendar.class.ReccuringInstance) {
|
||||||
await updateHandler(update)
|
await updateReccuringInstance(update, object as ReccuringInstance)
|
||||||
} else {
|
} else {
|
||||||
await client.update(object, update)
|
await client.update(object, update)
|
||||||
}
|
}
|
||||||
@ -124,94 +123,6 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updatePast (ops: DocumentUpdate<Event>) {
|
|
||||||
const obj = object as ReccuringInstance
|
|
||||||
const origin = await client.findOne(calendar.class.ReccuringEvent, {
|
|
||||||
eventId: obj.recurringEventId,
|
|
||||||
space: obj.space
|
|
||||||
})
|
|
||||||
if (origin !== undefined) {
|
|
||||||
await client.addCollection(
|
|
||||||
calendar.class.ReccuringEvent,
|
|
||||||
origin.space,
|
|
||||||
origin.attachedTo,
|
|
||||||
origin.attachedToClass,
|
|
||||||
origin.collection,
|
|
||||||
{
|
|
||||||
...origin,
|
|
||||||
date: obj.date,
|
|
||||||
dueDate: obj.dueDate,
|
|
||||||
...ops,
|
|
||||||
eventId: generateEventId()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
const targetDate = ops.date ?? obj.date
|
|
||||||
await client.update(origin, {
|
|
||||||
rules: [{ ...origin.rules[0], endDate: targetDate - DAY }],
|
|
||||||
rdate: origin.rdate.filter((p) => p < targetDate)
|
|
||||||
})
|
|
||||||
const instances = await client.findAll(calendar.class.ReccuringInstance, {
|
|
||||||
recurringEventId: origin.eventId,
|
|
||||||
date: { $gte: targetDate }
|
|
||||||
})
|
|
||||||
for (const instance of instances) {
|
|
||||||
await client.remove(instance)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateHandler (ops: DocumentUpdate<ReccuringEvent>) {
|
|
||||||
const obj = object as ReccuringInstance
|
|
||||||
if (obj.virtual !== true) {
|
|
||||||
await client.update(object, ops)
|
|
||||||
} else {
|
|
||||||
showPopup(UpdateRecInstancePopup, { currentAvailable: ops.rules === undefined }, undefined, async (res) => {
|
|
||||||
if (res !== null) {
|
|
||||||
if (res.mode === 'current') {
|
|
||||||
await client.addCollection(
|
|
||||||
obj._class,
|
|
||||||
obj.space,
|
|
||||||
obj.attachedTo,
|
|
||||||
obj.attachedToClass,
|
|
||||||
obj.collection,
|
|
||||||
{
|
|
||||||
title: obj.title,
|
|
||||||
description: obj.description,
|
|
||||||
date: obj.date,
|
|
||||||
dueDate: obj.dueDate,
|
|
||||||
allDay: obj.allDay,
|
|
||||||
participants: obj.participants,
|
|
||||||
externalParticipants: obj.externalParticipants,
|
|
||||||
originalStartTime: obj.originalStartTime,
|
|
||||||
recurringEventId: obj.recurringEventId,
|
|
||||||
reminders: obj.reminders,
|
|
||||||
location: obj.location,
|
|
||||||
eventId: obj.eventId,
|
|
||||||
access: 'owner',
|
|
||||||
rules: obj.rules,
|
|
||||||
exdate: obj.exdate,
|
|
||||||
rdate: obj.rdate,
|
|
||||||
...ops
|
|
||||||
},
|
|
||||||
obj._id
|
|
||||||
)
|
|
||||||
} else if (res.mode === 'all') {
|
|
||||||
const base = await client.findOne(calendar.class.ReccuringEvent, {
|
|
||||||
space: obj.space,
|
|
||||||
eventId: obj.recurringEventId
|
|
||||||
})
|
|
||||||
if (base !== undefined) {
|
|
||||||
await client.update(base, ops)
|
|
||||||
}
|
|
||||||
} else if (res.mode === 'next') {
|
|
||||||
await updatePast(ops)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
closePopup()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="eventPopup-container">
|
<div class="eventPopup-container">
|
||||||
|
@ -14,20 +14,19 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import calendar, { CalendarEventPresenter, Event } from '@hcengineering/calendar'
|
import calendar, { CalendarEventPresenter, Event } from '@hcengineering/calendar'
|
||||||
import { Doc, DocumentUpdate } from '@hcengineering/core'
|
import { Doc } from '@hcengineering/core'
|
||||||
import { getClient } from '@hcengineering/presentation'
|
import { getClient } from '@hcengineering/presentation'
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
MILLISECONDS_IN_MINUTE,
|
MILLISECONDS_IN_MINUTE,
|
||||||
deviceOptionsStore,
|
closeTooltip,
|
||||||
getEventPositionElement,
|
getEventPositionElement,
|
||||||
showPopup,
|
showPopup,
|
||||||
tooltip
|
tooltip
|
||||||
} from '@hcengineering/ui'
|
} from '@hcengineering/ui'
|
||||||
import view, { ObjectEditor } from '@hcengineering/view'
|
import view, { ObjectEditor } from '@hcengineering/view'
|
||||||
import { Menu } from '@hcengineering/view-resources'
|
import { Menu } from '@hcengineering/view-resources'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { calendarStore, isVisible } from '../utils'
|
||||||
import { calendarStore, isReadOnly, isVisible } from '../utils'
|
|
||||||
import EventPresenter from './EventPresenter.svelte'
|
import EventPresenter from './EventPresenter.svelte'
|
||||||
|
|
||||||
export let event: Event
|
export let event: Event
|
||||||
@ -56,93 +55,13 @@
|
|||||||
|
|
||||||
let div: HTMLDivElement
|
let div: HTMLDivElement
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
|
||||||
|
|
||||||
$: fontSize = $deviceOptionsStore.fontSize
|
|
||||||
|
|
||||||
function dragStart (e: DragEvent) {
|
|
||||||
if (readOnly) return
|
|
||||||
if (event.allDay) return
|
|
||||||
originDate = event.date
|
|
||||||
originDueDate = event.dueDate
|
|
||||||
const rect = div.getBoundingClientRect()
|
|
||||||
const topThreshold = rect.y + fontSize / 2
|
|
||||||
if (e.dataTransfer) {
|
|
||||||
e.dataTransfer.effectAllowed = 'move'
|
|
||||||
e.dataTransfer.dropEffect = 'move'
|
|
||||||
}
|
|
||||||
dragInitY = e.y
|
|
||||||
if (e.y < topThreshold) {
|
|
||||||
dragDirection = 'top'
|
|
||||||
} else {
|
|
||||||
const bottomThreshold = rect.y + rect.height - fontSize / 2
|
|
||||||
if (e.y > bottomThreshold) {
|
|
||||||
dragDirection = 'bottom'
|
|
||||||
} else {
|
|
||||||
dragDirection = 'mid'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let originDate = event.date
|
|
||||||
let originDueDate = event.dueDate
|
|
||||||
$: pixelPer15Min = hourHeight / 4
|
|
||||||
let dragInitY: number | undefined
|
|
||||||
let dragDirection: 'bottom' | 'mid' | 'top' | undefined
|
|
||||||
|
|
||||||
function drag (e: DragEvent) {
|
|
||||||
if (readOnly) return
|
|
||||||
if (event.allDay) return
|
|
||||||
if (dragInitY !== undefined) {
|
|
||||||
const diff = Math.floor((e.y - dragInitY) / pixelPer15Min)
|
|
||||||
if (diff) {
|
|
||||||
if (dragDirection !== 'bottom') {
|
|
||||||
const newValue = new Date(originDate).setMinutes(new Date(originDate).getMinutes() + 15 * diff)
|
|
||||||
if (dragDirection === 'top') {
|
|
||||||
if (newValue < event.dueDate) {
|
|
||||||
event.date = newValue
|
|
||||||
dispatch('resize')
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const newDue = new Date(originDueDate).setMinutes(new Date(originDueDate).getMinutes() + 15 * diff)
|
|
||||||
event.date = newValue
|
|
||||||
event.dueDate = newDue
|
|
||||||
dispatch('resize')
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const newDue = new Date(originDueDate).setMinutes(new Date(originDueDate).getMinutes() + 15 * diff)
|
|
||||||
if (newDue > event.date) {
|
|
||||||
event.dueDate = newDue
|
|
||||||
dispatch('resize')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function drop () {
|
|
||||||
const update: DocumentUpdate<Event> = {}
|
|
||||||
if (originDate !== event.date) {
|
|
||||||
update.date = event.date
|
|
||||||
}
|
|
||||||
if (originDueDate !== event.dueDate) {
|
|
||||||
update.dueDate = event.dueDate
|
|
||||||
}
|
|
||||||
if (Object.keys(update).length > 0) {
|
|
||||||
await client.update(event, {
|
|
||||||
dueDate: event.dueDate,
|
|
||||||
date: event.date
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showMenu (ev: MouseEvent) {
|
function showMenu (ev: MouseEvent) {
|
||||||
ev.preventDefault()
|
ev.preventDefault()
|
||||||
|
closeTooltip()
|
||||||
showPopup(Menu, { object: event }, getEventPositionElement(ev))
|
showPopup(Menu, { object: event }, getEventPositionElement(ev))
|
||||||
}
|
}
|
||||||
|
|
||||||
$: visible = isVisible(event, $calendarStore)
|
$: visible = isVisible(event, $calendarStore)
|
||||||
$: readOnly = isReadOnly(event)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if event}
|
{#if event}
|
||||||
@ -152,14 +71,9 @@
|
|||||||
class="event-container"
|
class="event-container"
|
||||||
class:oneRow
|
class:oneRow
|
||||||
class:empty
|
class:empty
|
||||||
draggable={!event.allDay}
|
|
||||||
use:tooltip={{ component: EventPresenter, props: { value: event, hideDetails: !visible } }}
|
use:tooltip={{ component: EventPresenter, props: { value: event, hideDetails: !visible } }}
|
||||||
on:click|stopPropagation={click}
|
on:click|stopPropagation={click}
|
||||||
on:contextmenu={showMenu}
|
on:contextmenu={showMenu}
|
||||||
on:dragstart={dragStart}
|
|
||||||
on:drag={drag}
|
|
||||||
on:dragend={drop}
|
|
||||||
on:drop
|
|
||||||
>
|
>
|
||||||
{#if !empty && presenter?.presenter}
|
{#if !empty && presenter?.presenter}
|
||||||
<Component is={presenter.presenter} props={{ event, narrow, oneRow, hideDetails: !visible }} />
|
<Component is={presenter.presenter} props={{ event, narrow, oneRow, hideDetails: !visible }} />
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { Calendar, Event } from '@hcengineering/calendar'
|
import { Calendar, Event, ReccuringEvent, ReccuringInstance, generateEventId } from '@hcengineering/calendar'
|
||||||
import { IdMap, Timestamp, getCurrentAccount, toIdMap } from '@hcengineering/core'
|
import { IdMap, Timestamp, getCurrentAccount, toIdMap, DocumentUpdate } from '@hcengineering/core'
|
||||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||||
|
import { showPopup, closePopup, DAY } from '@hcengineering/ui'
|
||||||
import { writable } from 'svelte/store'
|
import { writable } from 'svelte/store'
|
||||||
import calendar from './plugin'
|
import calendar from './plugin'
|
||||||
|
import UpdateRecInstancePopup from './components/UpdateRecInstancePopup.svelte'
|
||||||
|
|
||||||
export function saveUTC (date: Timestamp): Timestamp {
|
export function saveUTC (date: Timestamp): Timestamp {
|
||||||
const utcdate = new Date(date)
|
const utcdate = new Date(date)
|
||||||
@ -76,3 +78,94 @@ function fillStores (): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fillStores()
|
fillStores()
|
||||||
|
|
||||||
|
export async function updatePast (ops: DocumentUpdate<Event>, object: ReccuringInstance): Promise<void> {
|
||||||
|
const client = getClient()
|
||||||
|
const origin = await client.findOne(calendar.class.ReccuringEvent, {
|
||||||
|
eventId: object.recurringEventId,
|
||||||
|
space: object.space
|
||||||
|
})
|
||||||
|
if (origin !== undefined) {
|
||||||
|
await client.addCollection(
|
||||||
|
calendar.class.ReccuringEvent,
|
||||||
|
origin.space,
|
||||||
|
origin.attachedTo,
|
||||||
|
origin.attachedToClass,
|
||||||
|
origin.collection,
|
||||||
|
{
|
||||||
|
...origin,
|
||||||
|
date: object.date,
|
||||||
|
dueDate: object.dueDate,
|
||||||
|
...ops,
|
||||||
|
eventId: generateEventId()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const targetDate = ops.date ?? object.date
|
||||||
|
await client.update(origin, {
|
||||||
|
rules: [{ ...origin.rules[0], endDate: targetDate - DAY }],
|
||||||
|
rdate: origin.rdate.filter((p) => p < targetDate)
|
||||||
|
})
|
||||||
|
const instances = await client.findAll(calendar.class.ReccuringInstance, {
|
||||||
|
recurringEventId: origin.eventId,
|
||||||
|
date: { $gte: targetDate }
|
||||||
|
})
|
||||||
|
for (const instance of instances) {
|
||||||
|
await client.remove(instance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateReccuringInstance (
|
||||||
|
ops: DocumentUpdate<ReccuringEvent>,
|
||||||
|
object: ReccuringInstance
|
||||||
|
): Promise<void> {
|
||||||
|
const client = getClient()
|
||||||
|
if (object.virtual !== true) {
|
||||||
|
await client.update(object, ops)
|
||||||
|
} else {
|
||||||
|
showPopup(UpdateRecInstancePopup, { currentAvailable: ops.rules === undefined }, undefined, async (res) => {
|
||||||
|
if (res !== null) {
|
||||||
|
if (res.mode === 'current') {
|
||||||
|
await client.addCollection(
|
||||||
|
object._class,
|
||||||
|
object.space,
|
||||||
|
object.attachedTo,
|
||||||
|
object.attachedToClass,
|
||||||
|
object.collection,
|
||||||
|
{
|
||||||
|
title: object.title,
|
||||||
|
description: object.description,
|
||||||
|
date: object.date,
|
||||||
|
dueDate: object.dueDate,
|
||||||
|
allDay: object.allDay,
|
||||||
|
participants: object.participants,
|
||||||
|
externalParticipants: object.externalParticipants,
|
||||||
|
originalStartTime: object.originalStartTime,
|
||||||
|
recurringEventId: object.recurringEventId,
|
||||||
|
reminders: object.reminders,
|
||||||
|
location: object.location,
|
||||||
|
eventId: object.eventId,
|
||||||
|
access: 'owner',
|
||||||
|
rules: object.rules,
|
||||||
|
exdate: object.exdate,
|
||||||
|
rdate: object.rdate,
|
||||||
|
...ops
|
||||||
|
},
|
||||||
|
object._id
|
||||||
|
)
|
||||||
|
} else if (res.mode === 'all') {
|
||||||
|
const base = await client.findOne(calendar.class.ReccuringEvent, {
|
||||||
|
space: object.space,
|
||||||
|
eventId: object.recurringEventId
|
||||||
|
})
|
||||||
|
if (base !== undefined) {
|
||||||
|
await client.update(base, ops)
|
||||||
|
}
|
||||||
|
} else if (res.mode === 'next') {
|
||||||
|
await updatePast(ops, object)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
closePopup()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user