Updated Calendar layout. (#3504)

Signed-off-by: Alexander Platov <sas_lord@mail.ru>
This commit is contained in:
Alexander Platov 2023-07-17 09:41:46 +03:00 committed by GitHub
parent ba6a3e8638
commit c7efa0d113
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 891 additions and 309 deletions

View File

@ -183,6 +183,7 @@
--theme-toggle-bg-hover: rgba(120, 120, 128, 0.64);
--theme-toggle-on-bg-color: #205dc2;
--theme-toggle-on-bg-hover: #1A53AF;
--theme-radio-bg-color: #343442;
--theme-error-color: #eb5757;
--theme-urgent-color: #F5694A;
@ -371,6 +372,7 @@
--theme-toggle-bg-hover: rgba(120, 120, 128, 0.64);
--theme-toggle-on-bg-color: #205dc2;
--theme-toggle-on-bg-hover: #1A53AF;
--theme-radio-bg-color: #E5E5E5;
--theme-error-color: #eb5757; // Dark
--theme-urgent-color: #F5694A;

View File

@ -902,6 +902,7 @@ a.no-line {
.background-button-noborder-bg-hover { background-color: var(--noborder-bg-hover); }
.background-primary-color { background-color: var(--accented-button-default); }
.background-content-accent-color { background-color: var(--accent-color); }
.background-comp-header-color { background-color: var(--theme-comp-header-color); }
.content-trans-color { color: var(--theme-trans-color); }
.content-darker-color { color: var(--theme-darker-color); }

View File

@ -87,7 +87,7 @@
on:click={openPopup}
>
<span slot="content" class="overflow-label disabled flex-grow text-left mr-2">
<Label label={selectedItem ? selectedItem.label : label} {params} />
<Label label={selectedItem ? selectedItem.label : label} params={selectedItem ? selectedItem.params : params} />
</span>
<svelte:fragment slot="iconRight">
<DropdownIcon size={'small'} fill={'var(--theme-dark-color)'} />

View File

@ -0,0 +1,431 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Timestamp } from '@hcengineering/core'
import { createEventDispatcher, onMount } from 'svelte'
import { resizeObserver, deviceOptionsStore as deviceInfo, Scroller } from '../..'
import {
MILLISECONDS_IN_DAY,
addZero,
day as getDay,
getMonday,
getWeekDayName,
areDatesEqual
} from './internal/DateUtils'
import { CalendarItem } from '../../types'
export let events: CalendarItem[]
export let mondayStart = true
export let selectedDate: Date = new Date()
export let currentDate: Date = selectedDate
export let displayedDaysCount = 7
export let displayedHours = 24
export let startFromWeekStart = true
export let weekFormat: 'narrow' | 'short' | 'long' | undefined = displayedDaysCount > 4 ? 'short' : 'long'
// export let startHour = 0
const dispatch = createEventDispatcher()
const todayDate = new Date()
$: fontSize = $deviceInfo.fontSize
$: weekMonday = startFromWeekStart
? getMonday(currentDate, mondayStart)
: new Date(new Date(currentDate).setHours(0, 0, 0, 0))
interface CalendarCell {
id: number
start: number
end: number
day: number
element?: HTMLDivElement
}
interface CalendarElement {
id: string
date: Timestamp
dueDate: Timestamp
cols: number
}
interface CalendarRow {
elements: CalendarElement[]
}
interface CalendarGrid {
columns: CalendarRow[]
}
const cells: CalendarCell[] = Array<CalendarCell>(displayedHours * 2 * displayedDaysCount)
let container: HTMLElement
let scroller: HTMLElement
let calendarWidth: number = 0
let calendarRect: DOMRect
let colWidth: number = 0
for (let hourOfDay = 0; hourOfDay < displayedHours; hourOfDay++) {
for (let line = 1; line >= 0; line--) {
for (let dayOfWeek = 0; dayOfWeek < displayedDaysCount; dayOfWeek++) {
const cell = line
? hourOfDay * 2 + displayedHours * 2 * dayOfWeek
: hourOfDay * 2 + displayedHours * 2 * dayOfWeek + 1
cells[cell] = {
id: cell,
start: line ? hourOfDay * 100 : hourOfDay * 100 + 30,
end: line ? hourOfDay * 100 + 30 : (hourOfDay + 1) * 100,
day: dayOfWeek
}
}
}
}
let newEvents = events
let grid: CalendarGrid[] = Array<CalendarGrid>(displayedDaysCount)
$: if (newEvents !== events) {
grid = new Array<CalendarGrid>(displayedDaysCount)
newEvents = events
}
$: newEvents
.filter((ev) => !ev.allDay)
.forEach((event, i, arr) => {
if (grid[event.day] === undefined) {
grid[event.day] = {
columns: [{ elements: [{ id: event.eventId, date: event.date, dueDate: event.dueDate, cols: 1 }] }]
}
} else {
const index = grid[event.day].columns.findIndex(
(col) => col.elements[col.elements.length - 1].dueDate <= event.date
)
if (index === -1) {
const intersects = grid[event.day].columns.filter((col) =>
checkIntersect(col.elements[col.elements.length - 1], event)
)
const size = intersects.length + 1
grid[event.day].columns.forEach((col) => {
if (checkIntersect(col.elements[col.elements.length - 1], event)) {
col.elements[col.elements.length - 1].cols = size
}
})
grid[event.day].columns.push({
elements: [{ id: event.eventId, date: event.date, dueDate: event.dueDate, cols: size }]
})
} else {
const intersects = grid[event.day].columns.filter((col) =>
checkIntersect(col.elements[col.elements.length - 1], event)
)
let maxCols = 1
intersects.forEach((col) => {
if (col.elements[col.elements.length - 1].cols > maxCols) {
maxCols = col.elements[col.elements.length - 1].cols
}
})
if (intersects.length >= maxCols) maxCols = intersects.length + 1
grid[event.day].columns.forEach((col) => {
if (checkIntersect(col.elements[col.elements.length - 1], event)) {
col.elements[col.elements.length - 1].cols = maxCols
}
})
grid[event.day].columns[index].elements.push({
id: event.eventId,
date: event.date,
dueDate: event.dueDate,
cols: maxCols
})
}
}
if (i === arr.length - 1) checkGrid()
})
const checkGrid = () => {
for (let i = 0; i < displayedDaysCount; i++) {
if (grid[i] === null || typeof grid[i] !== 'object' || grid[i].columns.length === 0) continue
for (let j = 0; j < grid[i].columns.length - 1; j++) {
grid[i].columns[j].elements.forEach((el) => {
grid[i].columns[j + 1].elements.forEach((nextEl) => {
if (checkIntersect(el, nextEl)) {
if (el.cols > nextEl.cols) nextEl.cols = el.cols
else el.cols = nextEl.cols
}
})
})
}
}
}
const checkSizes = (element: HTMLElement | Element) => {
calendarRect = element.getBoundingClientRect()
calendarWidth = calendarRect.width
colWidth = (calendarWidth - 3.5 * fontSize) / displayedDaysCount
}
const getCellByTime = (time: number, day: number, end: boolean = false): CalendarCell | undefined => {
return end
? cells.filter((cell) => cell.start < time && time <= cell.end && cell.day === day)[0]
: cells.filter((cell) => cell.start <= time && time < cell.end && cell.day === day)[0]
}
const calcTimeOffset = (
date: Timestamp | Date,
rect: DOMRect,
needCorrect: boolean,
end: boolean = false
): number => {
const mins = new Date(date).getMinutes()
if (mins === 0 || mins === 30) return end ? 2 : 1
else if (mins < 30) {
const res = end ? ((30 - mins) / 30) * rect.height : (mins / 30) * rect.height
return needCorrect ? res + 1 : res
} else {
const res = end ? ((60 - mins) / 30) * rect.height : ((mins - 30) / 30) * rect.height
return needCorrect ? res + 1 : res
}
}
const checkIntersect = (date1: CalendarItem | CalendarElement, date2: CalendarItem | CalendarElement): boolean => {
return (
(date2.date <= date1.date && date2.dueDate > date1.date) ||
(date2.date >= date1.date && date2.date < date1.dueDate)
)
}
const convertToTime = (date: Timestamp | Date): number => {
const temp = new Date(date)
return temp.getHours() * 100 + temp.getMinutes()
}
const getRect = (event: CalendarItem): { top: number; bottom: number; left: number; right: number } => {
const result = { top: 0, bottom: 0, left: 0, right: 0, width: 0 }
const checkDate = new Date(currentDate.getTime() + MILLISECONDS_IN_DAY * event.day)
const startDay = checkDate.setHours(0, 0, 0)
const endDay = checkDate.setHours(displayedHours - 1, 59, 59)
const startTime = event.date < startDay ? 0 : convertToTime(event.date)
const endTime = event.dueDate > endDay ? displayedHours * 100 : convertToTime(event.dueDate)
const startCell = getCellByTime(startTime, event.day)
const endCell = getCellByTime(endTime, event.day, true)
const scrollOffset = scroller?.scrollTop ?? 0
if (startCell?.element && endCell?.element) {
const rectStart = startCell.element.getBoundingClientRect()
const rectEnd = startCell.id === endCell.id ? rectStart : endCell.element.getBoundingClientRect()
const startTimeOffset = calcTimeOffset(event.date, rectStart, startCell.id !== endCell.id)
const endTimeOffset = calcTimeOffset(event.dueDate, rectEnd, startCell.id !== endCell.id, true)
result.top = rectStart.top - calendarRect.top + startTimeOffset + scrollOffset
result.bottom = calendarRect.bottom - rectEnd.bottom + endTimeOffset - scrollOffset
let cols = 1
let index: number = 0
grid[event.day].columns.forEach((col, i) =>
col.elements.forEach((el) => {
if (el.id === event.eventId) {
cols = el.cols
index = i
}
})
)
const elWidth = (rectStart.width - 0.25 * fontSize - (cols - 1) * 0.125 * fontSize) / cols
result.width = elWidth
result.left = rectStart.left - calendarRect.left + 0.125 * fontSize + index * elWidth + index * 0.125 * fontSize
result.right =
calendarRect.right -
rectEnd.right +
0.125 * fontSize +
(cols - index - 1) * elWidth +
(cols - index - 1) * 0.125 * fontSize
}
return result
}
onMount(() => {
if (container) checkSizes(container)
})
</script>
<Scroller bind:divScroll={scroller} fade={{ multipler: { top: 5.75, bottom: 0 } }}>
<div
bind:this={container}
class="calendar-container"
style:grid={`[header] 3.5rem [all-day] 2.25rem repeat(${
displayedHours * 2
}, [row-start] 2rem) / [time-col] 3.5rem repeat(${displayedDaysCount}, [col-start] 1fr)`}
use:resizeObserver={(element) => checkSizes(element)}
>
<div class="sticky-header head center"><span class="zone">CEST</span></div>
{#each [...Array(displayedDaysCount).keys()] as dayOfWeek}
{@const day = getDay(weekMonday, dayOfWeek)}
<div class="sticky-header head title" class:center={displayedDaysCount > 1}>
<span class="day" class:today={areDatesEqual(todayDate, day)}>{day.getDate()}</span>
<span class="weekday">{getWeekDayName(day, weekFormat)}</span>
</div>
{/each}
<div class="sticky-header center text-sm content-dark-color">All day</div>
{#each [...Array(displayedDaysCount).keys()] as dayOfWeek}
{@const day = getDay(weekMonday, dayOfWeek)}
{@const alldays = events.filter((ev) => ev.allDay && ev.day === dayOfWeek)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="sticky-header"
class:allday-container={alldays.length > 0}
style:width={`${colWidth}px`}
style:grid-template-columns={`repeat(${alldays.length}, minmax(0, 1fr))`}
on:click|stopPropagation={() =>
dispatch('create', { day, hour: -1, halfHour: false, date: new Date(day.setHours(0, 0, 0, 0)) })}
>
{#each alldays as ad}
<div class="allday-event">
<slot name="allday" id={ad.eventId} />
</div>
{/each}
</div>
{/each}
{#each [...Array(displayedHours).keys()] as hourOfDay}
{#each [...Array(2).keys()] as half}
{#if hourOfDay === 0 && half === 0}
<div class="clear-cell" />
{:else if hourOfDay < displayedHours - 1 && half}
<div class="time-cell" style:grid-row={`row-start ${hourOfDay * 2 + 2} / row-start ${hourOfDay * 2 + 4}`}>
{addZero(hourOfDay + 1)}:00
</div>
{:else if half}
<div class="clear-cell" />
{/if}
{#each [...Array(displayedDaysCount).keys()] as dayOfWeek}
{@const day = getDay(weekMonday, dayOfWeek)}
{@const cell = hourOfDay * 2 + displayedHours * 2 * dayOfWeek + half}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
bind:this={cells[cell].element}
class="empty-cell {half === 0 ? 'first-half' : 'second-half'}"
style:grid-column={`col-start ${dayOfWeek + 1} / ${dayOfWeek + 2}`}
on:click|stopPropagation={() => {
dispatch('create', {
day,
hour: hourOfDay,
halfHour: half === 1,
date: new Date(day.setHours(hourOfDay, half * 30, 0, 0))
})
}}
/>
{/each}
{/each}
{/each}
{#key calendarWidth}
{#each events.filter((ev) => !ev.allDay) as event, i}
{@const rect = getRect(event)}
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div
class="calendar-element"
style:top={`${rect.top}px`}
style:bottom={`${rect.bottom}px`}
style:left={`${rect.left}px`}
style:right={`${rect.right}px`}
tabindex={1000 + i}
>
<slot name="event" id={event.eventId} />
</div>
{/each}
{/key}
</div>
</Scroller>
<style lang="scss">
.first-half,
.second-half {
border-left: 1px solid var(--theme-divider-color);
}
.second-half {
border-bottom: 1px solid var(--theme-divider-color);
}
.empty-cell {
}
.clear-cell {
}
.time-cell {
display: inline-flex;
justify-content: center;
align-items: center;
font-size: 0.75rem;
color: var(--theme-dark-color);
}
.calendar-element {
position: absolute;
border-radius: 0.25rem;
}
.sticky-header {
position: sticky;
background-color: var(--theme-comp-header-color);
z-index: 10;
&.head {
top: 0;
}
&:not(.head) {
top: 3.5rem;
border-top: 1px solid var(--theme-divider-color);
border-bottom: 1px solid var(--theme-divider-color);
}
&.center {
justify-content: center;
}
&.title {
font-size: 1.125rem;
color: var(--theme-caption-color);
&.center {
justify-content: center;
}
&:not(.center) {
padding-left: 1.375rem;
}
.day.today {
padding: 0.375rem;
color: var(--accented-button-color);
background-color: #3871e0;
border-radius: 0.375rem;
}
.weekday {
margin-left: 0.25rem;
opacity: 0.4;
&::first-letter {
text-transform: uppercase;
}
}
}
&:not(.allday-container) {
display: flex;
align-items: center;
}
&.allday-container {
display: inline-grid;
justify-items: stretch;
gap: 0.125rem;
padding: 0.125rem;
max-width: 100%;
.allday-event {
background-color: red;
border-radius: 0.25rem;
}
}
.zone {
padding: 0.375rem;
font-size: 0.625rem;
color: var(--theme-dark-color);
background-color: rgba(64, 109, 223, 0.1);
border-radius: 0.25rem;
}
}
.calendar-container {
will-change: transform;
position: relative;
display: grid;
}
</style>

View File

@ -15,6 +15,7 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { areDatesEqual, day, firstDay, getWeekDayName, isWeekend, weekday } from './internal/DateUtils'
import { Scroller, defaultSP } from '../..'
export let mondayStart = true
export let weekFormat: 'narrow' | 'short' | 'long' | undefined = 'short'
@ -34,43 +35,45 @@
const todayDate = new Date()
</script>
<div class="month-calendar flex-grow">
<div class="days-of-week-header">
{#each [...Array(7).keys()] as dayOfWeek}
<div class="day-name">{getWeekDayName(day(firstDayOfCurrentMonth, dayOfWeek), weekFormat)}</div>
{/each}
</div>
<div class="days-of-month">
{#each [...Array(displayedWeeksCount).keys()] as weekIndex}
<Scroller fade={defaultSP}>
<div class="month-calendar flex-grow">
<div class="days-of-week-header">
{#each [...Array(7).keys()] as dayOfWeek}
{@const date = weekday(firstDayOfCurrentMonth, weekIndex, dayOfWeek)}
<div style={`grid-column-start: ${dayOfWeek + 1}; grid-row-start: ${weekIndex + 1}`}>
<div style={`display: flex; width: 100%; height: ${cellHeight ? `${cellHeight};` : '100%;'}`}>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="cell flex-center"
class:weekend={isWeekend(date)}
class:wrongMonth={date.getMonth() !== currentDate.getMonth()}
on:click={() => onSelect(date)}
>
{#if !$$slots.cell}
{date.getDate()}
{:else}
<slot
name="cell"
{date}
today={areDatesEqual(todayDate, date)}
selected={areDatesEqual(selectedDate, date)}
wrongMonth={date.getMonth() !== currentDate.getMonth()}
/>
{/if}
<div class="day-name">{getWeekDayName(day(firstDayOfCurrentMonth, dayOfWeek), weekFormat)}</div>
{/each}
</div>
<div class="days-of-month">
{#each [...Array(displayedWeeksCount).keys()] as weekIndex}
{#each [...Array(7).keys()] as dayOfWeek}
{@const date = weekday(firstDayOfCurrentMonth, weekIndex, dayOfWeek)}
<div style={`grid-column-start: ${dayOfWeek + 1}; grid-row-start: ${weekIndex + 1}`}>
<div style={`display: flex; width: 100%; height: ${cellHeight ? `${cellHeight};` : '100%;'}`}>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="cell flex-center"
class:weekend={isWeekend(date)}
class:wrongMonth={date.getMonth() !== currentDate.getMonth()}
on:click={() => onSelect(date)}
>
{#if !$$slots.cell}
{date.getDate()}
{:else}
<slot
name="cell"
{date}
today={areDatesEqual(todayDate, date)}
selected={areDatesEqual(selectedDate, date)}
wrongMonth={date.getMonth() !== currentDate.getMonth()}
/>
{/if}
</div>
</div>
</div>
</div>
{/each}
{/each}
{/each}
</div>
</div>
</div>
</Scroller>
<style lang="scss">
.month-calendar {

View File

@ -14,8 +14,7 @@
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import ui from '../../plugin'
import Label from '../Label.svelte'
import ui, { Scroller, Label } from '../..'
import { addZero, areDatesEqual, day as getDay, getMonday, getWeekDayName } from './internal/DateUtils'
export let mondayStart = true
@ -36,47 +35,49 @@
: new Date(new Date(currentDate).setHours(0, 0, 0, 0))
</script>
<table>
<thead class="scroller-thead">
<tr class="scroller-thead__tr">
<th><Label label={ui.string.HoursLabel} /></th>
{#each [...Array(displayedDaysCount).keys()] as dayOfWeek}
{@const day = getDay(weekMonday, dayOfWeek)}
<th>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="cursor-pointer uppercase flex-col-center"
class:today={areDatesEqual(todayDate, day)}
on:click={() => {
dispatch('select', day)
}}
>
<div class="flex-center">{getWeekDayName(day, 'short')}</div>
<div class="flex-center">{day.getDate()}</div>
</div>
</th>
{/each}
</tr>
</thead>
<tbody>
{#each [...Array(displayedHours).keys()] as hourOfDay}
<tr>
<td style="width: 50px;" class="calendar-td first">
{#if hourOfDay !== 0}
{addZero(hourOfDay)}:00
{/if}
</td>
{#each [...Array(displayedDaysCount).keys()] as dayIndex}
<td class="calendar-td cell" style={`height: ${cellHeight};`}>
{#if $$slots.cell}
<slot name="cell" date={getDay(weekMonday, dayIndex, hourOfDay * 60)} />
{/if}
</td>
<Scroller fade={{ multipler: { top: 3, bottom: 0 } }}>
<table>
<thead class="scroller-thead">
<tr class="scroller-thead__tr">
<th><Label label={ui.string.HoursLabel} /></th>
{#each [...Array(displayedDaysCount).keys()] as dayOfWeek}
{@const day = getDay(weekMonday, dayOfWeek)}
<th>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="cursor-pointer uppercase flex-col-center"
class:today={areDatesEqual(todayDate, day)}
on:click={() => {
dispatch('select', day)
}}
>
<div class="flex-center">{getWeekDayName(day, 'short')}</div>
<div class="flex-center">{day.getDate()}</div>
</div>
</th>
{/each}
</tr>
{/each}
</tbody>
</table>
</thead>
<tbody>
{#each [...Array(displayedHours).keys()] as hourOfDay}
<tr>
<td style="width: 50px;" class="calendar-td first">
{#if hourOfDay !== 0}
{addZero(hourOfDay)}:00
{/if}
</td>
{#each [...Array(displayedDaysCount).keys()] as dayIndex}
<td class="calendar-td cell" style:height={cellHeight}>
{#if $$slots.cell}
<slot name="cell" date={getDay(weekMonday, dayIndex, hourOfDay * 60)} />
{/if}
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
</Scroller>
<style lang="scss">
table {

View File

@ -14,6 +14,8 @@
-->
<script lang="ts">
import MonthCalendar from './MonthCalendar.svelte'
import Scroller from '../Scroller.svelte'
import { defaultSP } from '../..'
/**
* If passed, calendars will use monday as first day
@ -35,25 +37,27 @@
}
</script>
<div class="year-erp-calendar">
{#each [...Array(12).keys()] as m}
<div class="antiComponentBox flex-col flex-grow flex-wrap" style={`min-width: ${minWidth};`}>
<span class="month-caption">{getMonthName(month(currentDate, m))}</span>
<MonthCalendar
{cellHeight}
weekFormat="narrow"
bind:selectedDate
currentDate={month(currentDate, m)}
{mondayStart}
on:change
>
<svelte:fragment slot="cell" let:date let:today let:selected let:wrongMonth>
<slot name="cell" {date} {today} {selected} {wrongMonth} />
</svelte:fragment>
</MonthCalendar>
</div>
{/each}
</div>
<Scroller padding={'0 2.25rem'} fade={defaultSP}>
<div class="year-erp-calendar">
{#each [...Array(12).keys()] as m}
<div class="antiComponentBox flex-col flex-grow flex-wrap" style={`min-width: ${minWidth};`}>
<span class="month-caption">{getMonthName(month(currentDate, m))}</span>
<MonthCalendar
{cellHeight}
weekFormat="narrow"
bind:selectedDate
currentDate={month(currentDate, m)}
{mondayStart}
on:change
>
<svelte:fragment slot="cell" let:date let:today let:selected let:wrongMonth>
<slot name="cell" {date} {today} {selected} {wrongMonth} />
</svelte:fragment>
</MonthCalendar>
</div>
{/each}
</div>
</Scroller>
<style lang="scss">
.year-erp-calendar {

View File

@ -3,6 +3,6 @@
export let fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<polygon points="8,11.7 1.6,5.4 2.4,4.6 8,10.3 13.6,4.6 14.4,5.4 " />
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path d="M16 22L6 12L7.4 10.6L16 19.2L24.6 10.6L26 12L16 22Z" />
</svg>

View File

@ -168,6 +168,7 @@ export { default as Panel } from './components/Panel.svelte'
export { default as MonthCalendar } from './components/calendar/MonthCalendar.svelte'
export { default as YearCalendar } from './components/calendar/YearCalendar.svelte'
export { default as WeekCalendar } from './components/calendar/WeekCalendar.svelte'
export { default as DayCalendar } from './components/calendar/DayCalendar.svelte'
export { default as FocusHandler } from './components/FocusHandler.svelte'
export { default as ListView } from './components/ListView.svelte'

View File

@ -379,3 +379,15 @@ export interface DialogStep {
readonly component: AnyComponent | AnySvelteComponent
props?: Record<string, any>
}
/**
* @public
*/
export interface CalendarItem {
eventId: string
allDay: boolean
date: Timestamp
dueDate: Timestamp
day: number
access: 'freeBusyReader' | 'reader' | 'writer' | 'owner'
}

View File

@ -32,19 +32,23 @@
IconBack,
IconForward,
MonthCalendar,
Scroller,
CalendarItem,
DayCalendar,
WeekCalendar,
YearCalendar,
areDatesEqual,
defaultSP,
getMonday,
showPopup
DropdownLabelsIntl,
showPopup,
MILLISECONDS_IN_DAY
} from '@hcengineering/ui'
import { BuildModelKey } from '@hcengineering/view'
import { CalendarMode } from '../index'
import calendar from '../plugin'
import Day from './Day.svelte'
import Hour from './Hour.svelte'
import EventElement from './EventElement.svelte'
import { IntlString } from '@hcengineering/platform'
export let _class: Ref<Class<Doc>>
export let space: Ref<Space> | undefined = undefined
@ -64,6 +68,9 @@
function getFrom (date: Date, mode: CalendarMode): Timestamp {
switch (mode) {
case CalendarMode.Days: {
return new Date(date).setHours(0, 0, 0, 0)
}
case CalendarMode.Day: {
return new Date(date).setHours(0, 0, 0, 0)
}
@ -81,6 +88,9 @@
function getTo (date: Date, mode: CalendarMode): Timestamp {
switch (mode) {
case CalendarMode.Days: {
return new Date(date).setDate(date.getDate() + 1)
}
case CalendarMode.Day: {
return new Date(date).setDate(date.getDate() + 1)
}
@ -163,6 +173,10 @@
return
}
switch (mode) {
case CalendarMode.Days: {
currentDate.setDate(currentDate.getDate() + val * 3)
break
}
case CalendarMode.Day: {
currentDate.setDate(currentDate.getDate() + val)
break
@ -187,31 +201,8 @@
month: 'long'
}).format(date)
}
function getWeekName (date: Date): string {
const onejan = new Date(date.getFullYear(), 0, 1)
const week = Math.ceil(((date.getTime() - onejan.getTime()) / 86400000 + onejan.getDay() + 1) / 7)
return `W${week}`
}
let mode: CalendarMode = CalendarMode.Year
function label (date: Date, mode: CalendarMode): string {
switch (mode) {
case CalendarMode.Day: {
return `${date.getDate()} ${getMonthName(date)} ${date.getFullYear()}`
}
case CalendarMode.Week: {
return `${getWeekName(date)} ${getMonthName(date)} ${date.getFullYear()}`
}
case CalendarMode.Month: {
return `${getMonthName(date)} ${date.getFullYear()}`
}
case CalendarMode.Year: {
return `${date.getFullYear()}`
}
}
}
let mode: CalendarMode = CalendarMode.Days
function showCreateDialog (date: Date, withTime: boolean) {
if (createComponent === undefined) {
@ -221,13 +212,68 @@
}
let indexes = new Map<Ref<Event>, number>()
const ddItems: {
id: string | number
label: IntlString
mode: CalendarMode
params?: Record<string, any>
}[] = [
{ id: 'day', label: calendar.string.ModeDay, mode: CalendarMode.Day },
{ id: 'days', label: calendar.string.DueDays, mode: CalendarMode.Days, params: { days: 3 } },
{ id: 'week', label: calendar.string.ModeWeek, mode: CalendarMode.Week },
{ id: 'month', label: calendar.string.ModeMonth, mode: CalendarMode.Month },
{ id: 'year', label: calendar.string.ModeYear, mode: CalendarMode.Year }
]
const toCalendar = (events: Event[], date: Date, days: number = 1): CalendarItem[] => {
const result: CalendarItem[] = []
for (let day = 0; day < days; day++) {
const startDate = new Date(MILLISECONDS_IN_DAY * day + date.getTime()).setHours(0, 0, 0)
const lastDate = new Date(MILLISECONDS_IN_DAY * day + date.getTime()).setHours(23, 59, 59)
events.forEach((event) => {
const eventStart = event.allDay
? new Date(event.date + new Date().getTimezoneOffset() * 60 * 1000).getTime()
: event.date
const eventEnd = event.allDay
? new Date(event.dueDate + new Date().getTimezoneOffset() * 60 * 1000).getTime()
: event.dueDate
if ((eventStart <= startDate && eventEnd > startDate) || (eventStart >= startDate && eventStart < lastDate)) {
result.push({
eventId: event.eventId,
allDay: event.allDay,
date: event.date,
dueDate: event.dueDate,
day,
access: event.access
})
}
})
}
return result
}
</script>
<div class="text-lg fs-bold px-10 my-4 flex-no-shrink clear-mins">
{label(currentDate, mode)}
</div>
<div class="flex-between mb-4 px-10 flex-no-shrink clear-mins">
<div class="calendar-header">
<div class="title">
{getMonthName(currentDate)}
<span>{currentDate.getFullYear()}</span>
</div>
<div class="flex-row-center gap-2">
<DropdownLabelsIntl
items={ddItems.map((it) => {
return { id: it.id, label: it.label, params: it.params }
})}
size={'medium'}
selected={ddItems.find((it) => it.mode === mode)?.id}
on:selected={(e) => (mode = ddItems.find((it) => it.id === e.detail)?.mode ?? ddItems[0].mode)}
/>
<Button
label={calendar.string.Today}
on:click={() => {
inc(0)
}}
/>
<Button
icon={IconBack}
kind={'ghost'}
@ -235,13 +281,6 @@
inc(-1)
}}
/>
<Button
label={calendar.string.Today}
kind={'ghost'}
on:click={() => {
inc(0)
}}
/>
<Button
icon={IconForward}
kind={'ghost'}
@ -250,139 +289,176 @@
}}
/>
</div>
<div class="flex-row-center gap-2 clear-mins">
<Button
label={calendar.string.ModeDay}
on:click={() => {
mode = CalendarMode.Day
}}
/>
<Button
label={calendar.string.ModeWeek}
on:click={() => {
mode = CalendarMode.Week
}}
/>
<Button
label={calendar.string.ModeMonth}
on:click={() => {
mode = CalendarMode.Month
}}
/>
<Button
label={calendar.string.ModeYear}
on:click={() => {
mode = CalendarMode.Year
}}
/>
</div>
</div>
<Scroller
padding={'0 2.25rem'}
fade={mode === CalendarMode.Week || mode === CalendarMode.Day ? { multipler: { top: 3, bottom: 0 } } : defaultSP}
>
{#if mode === CalendarMode.Year}
<YearCalendar
{mondayStart}
cellHeight={'2.5rem'}
bind:selectedDate
bind:currentDate
on:change={(e) => {
currentDate = e.detail
if (areDatesEqual(selectedDate, currentDate)) {
mode = CalendarMode.Month
}
selectedDate = e.detail
}}
>
<svelte:fragment slot="cell" let:date let:today let:selected let:wrongMonth>
<Day
events={findEvents(objects, date)}
{date}
{_class}
{baseMenuClass}
{options}
{config}
{today}
{selected}
{wrongMonth}
{query}
/>
</svelte:fragment>
</YearCalendar>
{:else if mode === CalendarMode.Month}
<MonthCalendar {mondayStart} cellHeight={'8.5rem'} bind:selectedDate bind:currentDate>
<svelte:fragment slot="cell" let:date let:today let:selected let:wrongMonth>
<Day
events={findEvents(objects, date)}
{date}
size={'huge'}
{_class}
{baseMenuClass}
{options}
{config}
{today}
{selected}
{wrongMonth}
{query}
on:select={(e) => {
currentDate = e.detail
if (areDatesEqual(selectedDate, currentDate)) {
mode = CalendarMode.Day
}
selectedDate = e.detail
}}
on:create={(e) => {
showCreateDialog(e.detail, false)
}}
/>
</svelte:fragment>
</MonthCalendar>
{:else if mode === CalendarMode.Week}
<WeekCalendar
{mondayStart}
cellHeight={'4.5rem'}
bind:selectedDate
bind:currentDate
on:select={(e) => {
currentDate = e.detail
selectedDate = e.detail
mode = CalendarMode.Day
}}
>
<svelte:fragment slot="cell" let:date>
<Hour
events={findEvents(objects, date, true)}
{date}
bind:indexes
{#if mode === CalendarMode.Year}
<YearCalendar
{mondayStart}
cellHeight={'2.5rem'}
bind:selectedDate
bind:currentDate
on:change={(e) => {
currentDate = e.detail
if (areDatesEqual(selectedDate, currentDate)) {
mode = CalendarMode.Month
}
selectedDate = e.detail
}}
>
<svelte:fragment slot="cell" let:date let:today let:selected let:wrongMonth>
<Day
events={findEvents(objects, date)}
{date}
{_class}
{baseMenuClass}
{options}
{config}
{today}
{selected}
{wrongMonth}
{query}
/>
</svelte:fragment>
</YearCalendar>
{:else if mode === CalendarMode.Month}
<MonthCalendar {mondayStart} cellHeight={'8.5rem'} bind:selectedDate bind:currentDate>
<svelte:fragment slot="cell" let:date let:today let:selected let:wrongMonth>
<Day
events={findEvents(objects, date)}
{date}
size={'huge'}
{_class}
{baseMenuClass}
{options}
{config}
{today}
{selected}
{wrongMonth}
{query}
on:select={(e) => {
currentDate = e.detail
if (areDatesEqual(selectedDate, currentDate)) {
mode = CalendarMode.Day
}
selectedDate = e.detail
}}
on:create={(e) => {
showCreateDialog(e.detail, false)
}}
/>
</svelte:fragment>
</MonthCalendar>
{:else if mode === CalendarMode.Week}
<DayCalendar
events={toCalendar(objects, currentDate, 7)}
{mondayStart}
displayedDaysCount={7}
startFromWeekStart={false}
bind:selectedDate
bind:currentDate
on:create={(e) => showCreateDialog(e.detail.date, true)}
>
<svelte:fragment slot="allday" let:id>
{@const event = objects.find((event) => event.eventId === id)}
{#if event}
<EventElement
{event}
allday
on:create={(e) => {
showCreateDialog(e.detail, true)
}}
/>
</svelte:fragment>
</WeekCalendar>
{:else if mode === CalendarMode.Day}
<WeekCalendar
{/if}
</svelte:fragment>
<svelte:fragment slot="event" let:id>
{@const event = objects.find((event) => event.eventId === id)}
{#if event}
<EventElement
{event}
on:create={(e) => {
showCreateDialog(e.detail, true)
}}
/>
{/if}
</svelte:fragment>
</DayCalendar>
{:else if mode === CalendarMode.Day || mode === CalendarMode.Days}
{#key mode}
<DayCalendar
events={toCalendar(objects, currentDate, mode === CalendarMode.Days ? 3 : 1)}
{mondayStart}
displayedDaysCount={1}
displayedDaysCount={mode === CalendarMode.Days ? 3 : 1}
startFromWeekStart={false}
cellHeight={'4.5rem'}
bind:selectedDate
bind:currentDate
on:create={(e) => showCreateDialog(e.detail.date, true)}
>
<svelte:fragment slot="cell" let:date>
<Hour
events={findEvents(objects, date, true)}
{date}
bind:indexes
wide
on:create={(e) => {
showCreateDialog(e.detail, true)
}}
/>
<svelte:fragment slot="allday" let:id>
{@const event = objects.find((event) => event.eventId === id)}
{#if event}
<EventElement
{event}
allday
on:create={(e) => {
showCreateDialog(e.detail, true)
}}
/>
{/if}
</svelte:fragment>
</WeekCalendar>
{/if}
</Scroller>
<div class="min-h-4 max-h-4 h-4 flex-no-shrink" />
<svelte:fragment slot="event" let:id>
{@const event = objects.find((event) => event.eventId === id)}
{#if event}
<EventElement
{event}
on:create={(e) => {
showCreateDialog(e.detail, true)
}}
/>
{/if}
</svelte:fragment>
</DayCalendar>
{/key}
{:else if mode === CalendarMode.Day}
<WeekCalendar
{mondayStart}
displayedDaysCount={1}
startFromWeekStart={false}
cellHeight={'4.5rem'}
bind:selectedDate
bind:currentDate
>
<svelte:fragment slot="cell" let:date>
<Hour
events={findEvents(objects, date, true)}
{date}
bind:indexes
wide
on:create={(e) => {
showCreateDialog(e.detail, true)
}}
/>
</svelte:fragment>
</WeekCalendar>
{/if}
<!-- <div class="min-h-4 max-h-4 h-4 flex-no-shrink" /> -->
<style lang="scss">
.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1.75rem 0.75rem 2.25rem;
.title {
font-size: 1.25rem;
color: var(--theme-caption-color);
&::first-letter {
text-transform: uppercase;
}
span {
opacity: 0.4;
}
}
}
</style>

View File

@ -0,0 +1,78 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Event } from '@hcengineering/calendar'
import { MILLISECONDS_IN_MINUTE, addZero, showPanel, tooltip } from '@hcengineering/ui'
import view from '@hcengineering/view'
import EventPresenter from './EventPresenter.svelte'
export let event: Event
export let allday: boolean = false
$: startDate = new Date(event.date)
$: endDate = new Date(event.dueDate)
$: oneRow = event.dueDate - event.date <= MILLISECONDS_IN_MINUTE * 30 || allday
$: narrow = event.dueDate - event.date < MILLISECONDS_IN_MINUTE * 25
const getTime = (date: Date): string => {
return `${addZero(date.getHours())}:${addZero(date.getMinutes())}`
}
</script>
{#if event}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="event-container"
class:oneRow
use:tooltip={oneRow ? { component: EventPresenter, props: { value: event } } : {}}
on:click|stopPropagation={() => {
if (event) showPanel(view.component.EditDoc, event._id, event._class, 'content')
}}
>
{#if !narrow}
<b class:overflow-label={oneRow}>{event.title}</b>
{/if}
{#if !oneRow}
<span class="overflow-label text-sm">{getTime(startDate)}-{getTime(endDate)}</span>
{/if}
</div>
{/if}
<style lang="scss">
.event-container {
overflow: hidden;
display: flex;
flex-direction: column;
flex-grow: 1;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
font-size: 0.8125rem;
background-color: #f3f6fb;
border: 1px solid rgba(43, 81, 144, 0.2);
border-left: 0.25rem solid #2b5190;
border-radius: 0.25rem;
cursor: pointer;
&:not(.oneRow) {
padding: 0.25rem 1rem;
}
&.oneRow {
justify-content: center;
padding: 0 1rem;
}
}
</style>

View File

@ -17,25 +17,9 @@
import { Class, DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import { Asset, IntlString } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import {
AnyComponent,
Button,
Component,
Label,
Loading,
SearchEdit,
showPopup,
TabList,
IconAdd
} from '@hcengineering/ui'
import { AnyComponent, Button, Component, Label, Loading, showPopup, TabList, IconAdd } from '@hcengineering/ui'
import view, { Viewlet, ViewletPreference } from '@hcengineering/view'
import {
FilterButton,
getViewOptions,
setActiveViewletId,
ViewletSettingButton,
viewOptionStore
} from '@hcengineering/view-resources'
import { getViewOptions, setActiveViewletId, viewOptionStore } from '@hcengineering/view-resources'
import calendar from '../plugin'
// import { deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
@ -50,7 +34,7 @@
export let createLabel: IntlString | undefined = calendar.string.CreateEvent
const viewletQuery = createQuery()
let search = ''
const search = ''
let resultQuery: DocumentQuery<Event> = {}
let viewlets: WithLookup<Viewlet>[] = []
@ -106,8 +90,6 @@
}
})
// $: twoRows = $deviceInfo.twoRows
$: viewOptions = getViewOptions(selectedViewlet, $viewOptionStore)
</script>
@ -130,35 +112,25 @@
<Button icon={IconAdd} label={createLabel} kind={'accented'} on:click={showCreateDialog} />
</div>
</div>
<div class="ac-header full divide search-start">
<div class="ac-header-full small-gap">
<SearchEdit bind:value={search} on:change={() => updateResultQuery(search)} />
<!-- <ActionIcon icon={IconMoreH} size={'small'} /> -->
<div class="buttons-divider" />
<FilterButton {_class} />
</div>
<div class="ac-header-full medium-gap">
<ViewletSettingButton bind:viewOptions viewlet={selectedViewlet} />
<!-- <ActionIcon icon={IconMoreH} size={'small'} /> -->
</div>
</div>
{#if selectedViewlet?.$lookup?.descriptor?.component}
{#if loading}
<Loading />
{:else}
<Component
is={selectedViewlet.$lookup?.descriptor?.component}
props={{
_class,
space,
options: selectedViewlet.options,
config: preference?.config ?? selectedViewlet.config,
viewOptions,
viewlet: selectedViewlet,
query: resultQuery,
search,
createComponent
}}
/>
<div class="flex-col w-full h-full background-comp-header-color">
{#if selectedViewlet?.$lookup?.descriptor?.component}
{#if loading}
<Loading />
{:else}
<Component
is={selectedViewlet.$lookup?.descriptor?.component}
props={{
_class,
space,
options: selectedViewlet.options,
config: preference?.config ?? selectedViewlet.config,
viewOptions,
viewlet: selectedViewlet,
query: resultQuery,
search,
createComponent
}}
/>
{/if}
{/if}
{/if}
</div>

View File

@ -127,6 +127,7 @@ async function removePast (client: TxOperations, object: ReccuringInstance): Pro
export enum CalendarMode {
Day,
Days,
Week,
Month,
Year