mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-14 12:25:17 +00:00
Calendar Week view and some fixes (#1220)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
f003dcd5a0
commit
24dfc7f274
@ -33,6 +33,6 @@
|
||||
"@anticrm/tags-resources": "~0.6.0",
|
||||
"@anticrm/ui": "~0.6.0",
|
||||
"@anticrm/model-core": "~0.6.0",
|
||||
"@anticrm/model-view": "~0.6.0"
|
||||
"@anticrm/model-view": "~0.6.0"
|
||||
}
|
||||
}
|
||||
|
@ -407,6 +407,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.antiTable-body__border {
|
||||
border: 1px solid var(--theme-button-border-hovered);
|
||||
}
|
||||
|
||||
&.highlightRows .antiTable-body__row {
|
||||
&:hover, &.checking { background-color: var(--theme-table-bg-hover); }
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script type="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { areDatesEqual, day, firstDay, getWeekDayName, isWeekend, weekday } from './internal/DateUtils'
|
||||
|
||||
export let mondayStart = true
|
||||
@ -22,10 +23,13 @@
|
||||
export let currentDate: Date = new Date()
|
||||
export let displayedWeeksCount = 6
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: firstDayOfCurrentMonth = firstDay(currentDate, mondayStart)
|
||||
|
||||
function onSelect (date: Date) {
|
||||
value = date
|
||||
dispatch('change', value)
|
||||
}
|
||||
|
||||
const todayDate = new Date()
|
||||
|
103
packages/ui/src/components/calendar/WeekCalendar.svelte
Normal file
103
packages/ui/src/components/calendar/WeekCalendar.svelte
Normal file
@ -0,0 +1,103 @@
|
||||
<!--
|
||||
// Copyright © 2022 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 type="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { addZero, day, getMonday, getWeekDayName } from './internal/DateUtils'
|
||||
|
||||
export let mondayStart = true
|
||||
export let cellHeight: string | undefined = undefined
|
||||
export let value: Date = new Date()
|
||||
export let currentDate: Date = new Date()
|
||||
export let displayedDaysCount = 7
|
||||
export let displayedHours = 24
|
||||
// export let startHour = 0
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: weekMonday = getMonday(currentDate, mondayStart)
|
||||
|
||||
function onSelect (date: Date) {
|
||||
value = date
|
||||
dispatch('change', value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<table class="antiTable">
|
||||
<thead class="scroller-thead">
|
||||
<tr class="scroller-thead__tr">
|
||||
<th>Hours</th>
|
||||
{#each [...Array(displayedDaysCount).keys()] as dayOfWeek}
|
||||
<th>
|
||||
<div class="antiTable-cells">
|
||||
{getWeekDayName(day(weekMonday, dayOfWeek), 'short')}
|
||||
{day(weekMonday, dayOfWeek).getDate()}
|
||||
</div>
|
||||
</th>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if $$slots.cell}
|
||||
<slot name="header" date={day(weekMonday, 0)} days={displayedDaysCount} />
|
||||
{/if}
|
||||
{#each [...Array(displayedHours).keys()] as hourOfDay, row}
|
||||
<tr class="antiTable-body__row">
|
||||
<td style="width: 50px;" class="calendar-td">
|
||||
<div class="antiTable-cells__firstCell">
|
||||
{addZero(hourOfDay)}:00
|
||||
</div>
|
||||
</td>
|
||||
{#each [...Array(displayedDaysCount).keys()] as dayIndex}
|
||||
<td
|
||||
class="antiTable-body__border calendar-td cell"
|
||||
style={`height: ${cellHeight};`}
|
||||
on:click={(evt) => {
|
||||
onSelect(day(weekMonday, dayIndex))
|
||||
}}
|
||||
>
|
||||
{#if $$slots.cell}
|
||||
<slot name="cell" date={day(weekMonday, dayIndex, hourOfDay * 60)} />
|
||||
{/if}
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<style lang="scss">
|
||||
.calendar-td {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.selected {
|
||||
// border-radius: 3px;
|
||||
background-color: var(--primary-button-enabled);
|
||||
// border-color: var(--primary-button-focused-border);
|
||||
color: var(--primary-button-color);
|
||||
border-radius: 0.5rem;
|
||||
height: calc(100% - 5px);
|
||||
width: calc(100% - 5px);
|
||||
}
|
||||
.cell {
|
||||
width: 8rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.cell:hover:not(.wrongMonth) {
|
||||
// border: 1px solid var(--primary-button-focused-border);
|
||||
background-color: var(--primary-button-enabled);
|
||||
color: var(--primary-button-color);
|
||||
}
|
||||
</style>
|
@ -39,7 +39,7 @@
|
||||
{#each [...Array(12).keys()] as m}
|
||||
<div class="antiComponentBox mt-2 mb-2 ml-2 mr-2 flex-grow" style={`min-width: ${minWidth};`}>
|
||||
{getMonthName(month(value, m))}
|
||||
<MonthCalendar {cellHeight} weekFormat="narrow" bind:value currentDate={month(currentDate, m)} {mondayStart}>
|
||||
<MonthCalendar {cellHeight} weekFormat="narrow" bind:value currentDate={month(currentDate, m)} {mondayStart} on:change>
|
||||
<!----> eslint-disable-next-line no-undef -->
|
||||
<svelte:fragment slot="cell" let:date={date}>
|
||||
<slot name="cell" date={date} />
|
||||
|
@ -11,9 +11,9 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
|
||||
const DAYS_IN_WEEK = 7
|
||||
const MILLISECONDS_IN_DAY = 86400000
|
||||
const MILLISECONDS_IN_MINUTE = 60000
|
||||
|
||||
export function firstDay (date: Date, mondayStart: boolean): Date {
|
||||
const firstDayOfMonth = new Date(date)
|
||||
@ -40,8 +40,8 @@ export function getWeekDayName (weekDay: Date, weekFormat: 'narrow' | 'short' |
|
||||
}).format(weekDay)
|
||||
}
|
||||
|
||||
export function day (firstDay: Date, offset: number): Date {
|
||||
return new Date(firstDay.getTime() + offset * MILLISECONDS_IN_DAY)
|
||||
export function day (firstDay: Date, offset: number, minutes?: number): Date {
|
||||
return new Date(firstDay.getTime() + offset * MILLISECONDS_IN_DAY + (minutes ?? 0) * MILLISECONDS_IN_MINUTE)
|
||||
}
|
||||
|
||||
export function weekday (firstDay: Date, w: number, d: number): Date {
|
||||
@ -67,3 +67,16 @@ export function getMonthName (date: Date): string {
|
||||
const locale = new Intl.NumberFormat().resolvedOptions().locale
|
||||
return new Intl.DateTimeFormat(locale, { month: 'long' }).format(date)
|
||||
}
|
||||
export function getMonday (d: Date, mondayStart: boolean): Date {
|
||||
d = new Date(d)
|
||||
const day = d.getDay()
|
||||
const diff = d.getDate() - day + (day === 0 ? -6 : 1) // adjust when day is sunday
|
||||
return new Date(d.setDate(diff))
|
||||
}
|
||||
|
||||
export function addZero (value: number): string {
|
||||
if (value < 10) {
|
||||
return `0${value}`
|
||||
}
|
||||
return `${value}`
|
||||
}
|
||||
|
@ -120,3 +120,4 @@ export * from './colors'
|
||||
|
||||
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'
|
||||
|
@ -16,9 +16,11 @@
|
||||
import { Event } from '@anticrm/calendar'
|
||||
import { Class, Doc, DocumentQuery, FindOptions, Ref, SortingOrder, Space } from '@anticrm/core'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import { Button, IconBack, IconForward, MonthCalendar, ScrollBox, YearCalendar } from '@anticrm/ui'
|
||||
import { Button, IconBack, IconForward, MonthCalendar, ScrollBox, WeekCalendar, YearCalendar } from '@anticrm/ui'
|
||||
import Scroller from '@anticrm/ui/src/components/Scroller.svelte'
|
||||
import calendar from '../plugin'
|
||||
import Day from './Day.svelte'
|
||||
import Hour from './Hour.svelte'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let space: Ref<Space> | undefined = undefined
|
||||
@ -38,7 +40,7 @@
|
||||
|
||||
let loading = false
|
||||
let resultQuery: DocumentQuery<Event>
|
||||
$: spaceOpt = (space ? { space } : {})
|
||||
$: spaceOpt = space ? { space } : {}
|
||||
$: resultQuery = search === '' ? { ...query, ...spaceOpt } : { ...query, $search: search, ...spaceOpt }
|
||||
|
||||
let objects: Event[] = []
|
||||
@ -73,12 +75,48 @@
|
||||
)
|
||||
}
|
||||
|
||||
function findEvents (events: Event[], date: Date): Event[] {
|
||||
return events.filter(
|
||||
(it) => areDatesLess(new Date(it.date), date) && areDatesLess(date, new Date(it.dueDate ?? it.date))
|
||||
function isSameDay (firstDate: Date, secondDate: Date): boolean {
|
||||
return (
|
||||
firstDate.getFullYear() === secondDate.getFullYear() &&
|
||||
firstDate.getMonth() === secondDate.getMonth() &&
|
||||
firstDate.getDate() === secondDate.getDate()
|
||||
)
|
||||
}
|
||||
|
||||
function findEvents (events: Event[], date: Date, minutes = false): Event[] {
|
||||
return events.filter((it) => {
|
||||
const d1 = new Date(it.date)
|
||||
const d2 = new Date(it.dueDate ?? it.date)
|
||||
const inDays = areDatesLess(d1, date) && areDatesLess(date, d2)
|
||||
|
||||
if (minutes) {
|
||||
if (isSameDay(d1, date)) {
|
||||
return (date.getTime() < d1.getTime() && d1.getTime() < date.getTime() + 3600 * 1000) ||
|
||||
(d1.getTime() < date.getTime() && date.getTime() <= d2.getTime())
|
||||
}
|
||||
|
||||
if (isSameDay(d2, date)) {
|
||||
return (date.getTime() < d2.getTime() && d2.getTime() < date.getTime() + 3600000) ||
|
||||
(date.getTime() < d2.getTime() && d1.getTime() < date.getTime())
|
||||
}
|
||||
|
||||
// Somethere in middle
|
||||
return inDays
|
||||
}
|
||||
return inDays
|
||||
})
|
||||
}
|
||||
|
||||
function findDayEvents (events: Event[], date: Date): Event[] {
|
||||
return events.filter((it) => {
|
||||
const d1 = new Date(it.date)
|
||||
const d2 = new Date(it.dueDate ?? it.date)
|
||||
const inDays = areDatesLess(d1, date) && areDatesLess(date, d2)
|
||||
|
||||
return inDays && !isSameDay(d1, date) && !isSameDay(d2, date)
|
||||
})
|
||||
}
|
||||
|
||||
interface ShiftType {
|
||||
yearShift: number
|
||||
monthShift: number
|
||||
@ -128,6 +166,12 @@
|
||||
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}`
|
||||
}
|
||||
|
||||
function currentDate (date: Date, shifts: ShiftType): Date {
|
||||
const res = new Date(date)
|
||||
@ -152,7 +196,7 @@
|
||||
return `${date.getDate()} ${getMonthName(date)} ${date.getFullYear()}`
|
||||
}
|
||||
case CalendarMode.Week: {
|
||||
return `${getMonthName(date)} ${date.getFullYear()}`
|
||||
return `${getWeekName(date)} ${getMonthName(date)} ${date.getFullYear()}`
|
||||
}
|
||||
case CalendarMode.Month: {
|
||||
return `${getMonthName(date)} ${date.getFullYear()}`
|
||||
@ -175,14 +219,14 @@
|
||||
on:click={() => {
|
||||
mode = CalendarMode.Day
|
||||
}}
|
||||
/>
|
||||
/> -->
|
||||
<Button
|
||||
size={'small'}
|
||||
label={calendar.string.ModeWeek}
|
||||
on:click={() => {
|
||||
mode = CalendarMode.Week
|
||||
}}
|
||||
/> -->
|
||||
/>
|
||||
<Button
|
||||
size={'small'}
|
||||
label={calendar.string.ModeMonth}
|
||||
@ -238,7 +282,15 @@
|
||||
|
||||
{#if mode === CalendarMode.Year}
|
||||
<ScrollBox bothScroll>
|
||||
<YearCalendar {mondayStart} cellHeight={'2.5rem'} bind:value currentDate={currentDate(date, shifts)}>
|
||||
<YearCalendar
|
||||
{mondayStart}
|
||||
cellHeight={'2.5rem'}
|
||||
bind:value
|
||||
currentDate={currentDate(date, shifts)}
|
||||
on:change={(evt) => {
|
||||
date = evt.detail
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="cell" let:date>
|
||||
<Day
|
||||
events={findEvents(objects, date)}
|
||||
@ -254,7 +306,15 @@
|
||||
</ScrollBox>
|
||||
{:else if mode === CalendarMode.Month}
|
||||
<div class="flex flex-grow">
|
||||
<MonthCalendar {mondayStart} cellHeight={'8.5rem'} bind:value currentDate={currentDate(date, shifts)}>
|
||||
<MonthCalendar
|
||||
{mondayStart}
|
||||
cellHeight={'8.5rem'}
|
||||
bind:value
|
||||
currentDate={currentDate(date, shifts)}
|
||||
on:change={(evt) => {
|
||||
date = evt.detail
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="cell" let:date>
|
||||
<Day
|
||||
events={findEvents(objects, date)}
|
||||
@ -269,4 +329,37 @@
|
||||
</svelte:fragment>
|
||||
</MonthCalendar>
|
||||
</div>
|
||||
{:else if mode === CalendarMode.Week}
|
||||
<Scroller>
|
||||
<WeekCalendar
|
||||
{mondayStart}
|
||||
cellHeight={'4.5rem'}
|
||||
bind:value
|
||||
currentDate={currentDate(date, shifts)}
|
||||
on:change={(evt) => {
|
||||
date = evt.detail
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="cell" let:date>
|
||||
<Hour
|
||||
events={findEvents(objects, date, true)}
|
||||
{date}
|
||||
{_class}
|
||||
{baseMenuClass}
|
||||
{options}
|
||||
{config}
|
||||
query={resultQuery}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="header" let:date let:days>
|
||||
<tr class="antiTable-body__row">
|
||||
<td class="whitespace-nowrap">All day</td>
|
||||
{#each [...Array(days).keys()] as day}
|
||||
<td class="antiTable-body__border" />
|
||||
{/each}
|
||||
</tr>
|
||||
</svelte:fragment>
|
||||
</WeekCalendar>
|
||||
</Scroller>
|
||||
{/if}
|
||||
|
@ -80,7 +80,6 @@
|
||||
<style lang="scss">
|
||||
.cell {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
|
||||
.marker {
|
||||
|
83
plugins/calendar-resources/src/components/Hour.svelte
Normal file
83
plugins/calendar-resources/src/components/Hour.svelte
Normal file
@ -0,0 +1,83 @@
|
||||
<!--
|
||||
// Copyright © 2022 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 '@anticrm/calendar'
|
||||
import { Class, Doc, DocumentQuery, FindOptions, Ref } from '@anticrm/core'
|
||||
import { Tooltip } from '@anticrm/ui'
|
||||
import { addZero } from '@anticrm/ui/src/components/calendar/internal/DateUtils'
|
||||
import calendar from '../plugin'
|
||||
import EventsPopup from './EventsPopup.svelte'
|
||||
|
||||
export let events: Event[]
|
||||
export let date: Date
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let query: DocumentQuery<Event> = {}
|
||||
export let options: FindOptions<Event> | undefined = undefined
|
||||
export let baseMenuClass: Ref<Class<Event>> | undefined = undefined
|
||||
export let config: string[]
|
||||
|
||||
$: sorted = Array.from(events).sort((a, b) => a.date - b.date)
|
||||
|
||||
function from (eDate: number, date: Date): string {
|
||||
const dd = new Date(Math.max(eDate, date.getTime()))
|
||||
return `${addZero(date.getHours())}:${addZero(dd.getMinutes())}`
|
||||
}
|
||||
|
||||
function to (dueDate: number, date:Date): string {
|
||||
return `${addZero(date.getHours())}:${addZero(Math.min(59, Math.floor((dueDate - date.getTime()) / 60000)))}`
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if events.length > 0}
|
||||
<Tooltip
|
||||
fill={true}
|
||||
label={calendar.string.Events}
|
||||
component={EventsPopup}
|
||||
props={{ value: events, _class, query, options, baseMenuClass, config }}
|
||||
>
|
||||
<div class="cell">
|
||||
<div class="flex flex-col flex-grow">
|
||||
{#each sorted.slice(0, 4) as e, ei}
|
||||
<div class="overflow-label flex flex-between">
|
||||
{e.title}
|
||||
<div>
|
||||
{from(e.date, date)}
|
||||
-
|
||||
{to(e.dueDate ?? e.date, date)}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{#if events.length > 4}
|
||||
And {events.length - 4} more
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.cell {
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
background-color: var(--theme-dialog-accent);
|
||||
}
|
||||
.title {
|
||||
align-self: flex-start;
|
||||
}
|
||||
</style>
|
@ -3,7 +3,6 @@
|
||||
"version": "0.6.0",
|
||||
"main": "src/index.ts",
|
||||
"author": "Anticrm Platform Contributors",
|
||||
"template": "@anticrm/platform-package",
|
||||
"license": "EPL-2.0",
|
||||
"scripts": {
|
||||
"build": "echo 'no build for ui'",
|
||||
|
@ -4,6 +4,7 @@
|
||||
"main": "lib/index.js",
|
||||
"author": "Anticrm Platform Contributors",
|
||||
"license": "EPL-2.0",
|
||||
"template": "@anticrm/platform-package",
|
||||
"scripts": {
|
||||
"build": "heft build",
|
||||
"build:watch": "tsc",
|
||||
|
@ -11,7 +11,7 @@
|
||||
"@anticrm/platform-rig": "~0.6.0",
|
||||
"svelte-loader":"^3.1.2",
|
||||
"sass":"^1.37.5",
|
||||
"svelte-preprocess":"^4.7.4",
|
||||
"svelte-preprocess":"^4.10.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
||||
"@typescript-eslint/parser": "^5.4.0",
|
||||
"eslint-config-standard-with-typescript": "^21.0.1",
|
||||
@ -31,6 +31,8 @@
|
||||
"svelte": "^3.37.0"
|
||||
},
|
||||
"#replaces": [
|
||||
".eslintrc.js"
|
||||
".eslintrc.js",
|
||||
"postcss.config.js",
|
||||
"svelte.config.js"
|
||||
]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user