mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-22 11:01:54 +03:00
Update DatePicker (#1337)
Signed-off-by: Alexander Platov <sas_lord@mail.ru>
This commit is contained in:
parent
b695538e12
commit
28329f74bc
@ -428,7 +428,7 @@ a.no-line {
|
||||
.fs-title {
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
color: var(--theme-caption-color);
|
||||
color: var(--caption-color);
|
||||
user-select: none;
|
||||
}
|
||||
.trans-title {
|
||||
|
@ -63,6 +63,7 @@
|
||||
box-sizing: border-box;
|
||||
scrollbar-color: var(--theme-menu-color) var(--theme-bg-color);
|
||||
scrollbar-width: thin;
|
||||
--font-family: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@ -99,7 +100,7 @@ body {
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
|
||||
font-family: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto;
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
font-size: .875rem;
|
||||
color: var(--content-color);
|
||||
|
@ -19,6 +19,11 @@
|
||||
"StartDate": "Start date",
|
||||
"TargetDate": "Target date",
|
||||
"Overdue": "Overdue",
|
||||
"DueDate": "Due date",
|
||||
"AddDueDate": "Add due date",
|
||||
"EditDueDate": "Edit due date",
|
||||
"SaveDueDate": "Save due date",
|
||||
"IssueNeedsToBeCompletedByThisDate": "Issue needs to be completed by this date",
|
||||
"English": "English",
|
||||
"Russian": "Russian",
|
||||
"MinutesBefore": "{minutes, plural, =1 {a minute before} other {# minutes before}}",
|
||||
|
@ -19,6 +19,11 @@
|
||||
"StartDate": "Дата начала",
|
||||
"TargetDate": "Дата окончания",
|
||||
"Overdue": "Просрочено",
|
||||
"DueDate": "Срок",
|
||||
"AddDueDate": "Установить дату",
|
||||
"EditDueDate": "Изменить дату",
|
||||
"SaveDueDate": "Сохранить дату",
|
||||
"IssueNeedsToBeCompletedByThisDate": "Задача должна быть завершена к этой дате",
|
||||
"English": "Английский",
|
||||
"Russian": "Русский",
|
||||
"MinutesBefore": "{minutes, plural, =1 {за минуту} other {за # минут}}",
|
||||
|
@ -44,10 +44,11 @@
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
color: var(--dark-color);
|
||||
&.invisible { opacity: 0; }
|
||||
}
|
||||
&:hover .icon {
|
||||
color: var(--theme-caption-color);
|
||||
color: var(--accent-color);
|
||||
opacity: 1;
|
||||
}
|
||||
&:focus {
|
||||
|
@ -22,7 +22,7 @@
|
||||
|
||||
export let label: IntlString | undefined = undefined
|
||||
export let kind: 'primary' | 'secondary' | 'no-border' | 'transparent' | 'dangerous' = 'secondary'
|
||||
export let size: 'small' | 'medium' | 'large' | 'large' = 'medium'
|
||||
export let size: 'small' | 'medium' | 'large' | 'x-large' = 'medium'
|
||||
export let icon: Asset | AnySvelteComponent | undefined = undefined
|
||||
export let justify: 'left' | 'center' = 'center'
|
||||
export let disabled: boolean = false
|
||||
@ -148,6 +148,7 @@
|
||||
.btn-icon { color: var(--caption-color); }
|
||||
}
|
||||
&:disabled {
|
||||
color: var(--content-color);
|
||||
background-color: #30323655;
|
||||
cursor: default;
|
||||
&:hover {
|
||||
|
@ -83,10 +83,11 @@
|
||||
modalHTML.style.top = `calc(${rect.top}px + 0.5rem)`
|
||||
modalHTML.style.bottom = '0.75rem'
|
||||
modalHTML.style.right = '0.75rem'
|
||||
} else if (props.element === 'float') {
|
||||
modalHTML.style.top = '4rem'
|
||||
modalHTML.style.bottom = '4rem'
|
||||
modalHTML.style.right = '4rem'
|
||||
} else if (props.element === 'top') {
|
||||
modalHTML.style.top = '15vh'
|
||||
modalHTML.style.left = '50%'
|
||||
modalHTML.style.transform = 'translateX(-50%)'
|
||||
show = true
|
||||
} else if (props.element === 'account') {
|
||||
modalHTML.style.bottom = '2.75rem'
|
||||
modalHTML.style.left = '5rem'
|
||||
|
@ -78,10 +78,11 @@
|
||||
modalHTML.style.top = '0'
|
||||
modalHTML.style.bottom = '0'
|
||||
modalHTML.style.right = '0'
|
||||
} else if (element === 'float') {
|
||||
modalHTML.style.top = '4rem'
|
||||
modalHTML.style.bottom = '4rem'
|
||||
modalHTML.style.right = '4rem'
|
||||
} else if (element === 'top') {
|
||||
modalHTML.style.top = '15vh'
|
||||
modalHTML.style.left = '50%'
|
||||
modalHTML.style.transform = 'translateX(-50%)'
|
||||
show = true
|
||||
} else if (element === 'account') {
|
||||
modalHTML.style.bottom = '2.75rem'
|
||||
modalHTML.style.left = '5rem'
|
||||
@ -92,7 +93,7 @@
|
||||
modalHTML.style.right = '0'
|
||||
}
|
||||
} else {
|
||||
modalHTML.style.top = '25%'
|
||||
modalHTML.style.top = '50%'
|
||||
modalHTML.style.left = '50%'
|
||||
modalHTML.style.transform = 'translate(-50%, -50%)'
|
||||
show = true
|
||||
|
@ -15,13 +15,15 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import type { IntlString } from '@anticrm/platform'
|
||||
import Label from '../Label.svelte'
|
||||
import ui, { Label } from '../..'
|
||||
import DatePresenter from './DatePresenter.svelte'
|
||||
import DatePopup from './DatePopup.svelte'
|
||||
|
||||
export let title: IntlString
|
||||
export let value: number | null | undefined = null
|
||||
export let withTime: boolean = false
|
||||
export let icon: 'normal' | 'warning' | 'overdue' = 'normal'
|
||||
export let labelOver: IntlString | undefined = undefined
|
||||
export let labelNull: IntlString = ui.string.NoDate
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@ -36,6 +38,6 @@
|
||||
<div class="antiSelect antiWrapper cursor-default">
|
||||
<div class="flex-col">
|
||||
<span class="label mb-1"><Label label={title} /></span>
|
||||
<DatePresenter {value} {withTime} editable on:change={changeValue} />
|
||||
<DatePresenter {value} {withTime} {icon} {labelOver} {labelNull} editable on:change={changeValue} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -13,189 +13,441 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { translate } from '@anticrm/platform'
|
||||
import { afterUpdate, createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||
import ui from '../..'
|
||||
import type { TCellStyle, ICell } from './internal/DateUtils'
|
||||
import { firstDay, day, getWeekDayName, areDatesEqual, getMonthName, daysInMonth } from './internal/DateUtils'
|
||||
import { createEventDispatcher, afterUpdate } from 'svelte'
|
||||
import { IntlString } from '@anticrm/platform'
|
||||
import ui, { Button, ActionIcon, IconClose, Icon, Label } from '../..'
|
||||
import { daysInMonth } from './internal/DateUtils'
|
||||
import MonthSquare from './MonthSquare.svelte'
|
||||
|
||||
export let value: number | null | undefined
|
||||
export let currentDate: Date | null
|
||||
export let withTime: boolean = false
|
||||
export let mondayStart: boolean = true
|
||||
export let editable: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let currentDate: Date = new Date(value ?? Date.now())
|
||||
let days: Array<ICell> = []
|
||||
let scrollDiv: HTMLElement
|
||||
|
||||
$: if (value) currentDate = new Date(value)
|
||||
$: firstDayOfCurrentMonth = firstDay(currentDate, mondayStart)
|
||||
|
||||
const getNow = (): Date => {
|
||||
const tempDate = new Date(Date.now())
|
||||
return new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate())
|
||||
let popupCaption: IntlString = currentDate != null ? ui.string.EditDueDate : ui.string.AddDueDate
|
||||
type TEdits = 'day' | 'month' | 'year' | 'hour' | 'min'
|
||||
interface IEdits {
|
||||
id: TEdits
|
||||
value: number
|
||||
el?: HTMLElement
|
||||
}
|
||||
const today: Date = getNow()
|
||||
let todayString: string
|
||||
translate(ui.string.Today, {}).then(res => todayString = res)
|
||||
const editsType: TEdits[] = ['day', 'month', 'year', 'hour', 'min']
|
||||
const getIndex = (id: TEdits): number => editsType.indexOf(id)
|
||||
const today: Date = new Date(Date.now())
|
||||
let selected: TEdits | null = 'day'
|
||||
let startTyping: boolean = false
|
||||
let edits: IEdits[] = editsType.map(edit => { return { id: edit, value: -1 } })
|
||||
let viewDate: Date = currentDate ?? today
|
||||
let viewDateSec: Date
|
||||
|
||||
|
||||
const getDateStyle = (date: Date): TCellStyle => {
|
||||
if (value !== undefined && value !== null && areDatesEqual(currentDate, date)) return 'selected'
|
||||
return 'not-selected'
|
||||
}
|
||||
|
||||
const renderCellStyles = (): void => {
|
||||
days = []
|
||||
for (let i = 1; i <= daysInMonth(currentDate); i++) {
|
||||
const tempDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), i)
|
||||
days.push({
|
||||
dayOfWeek: (tempDate.getDay() === 0) ? 7 : tempDate.getDay(),
|
||||
style: getDateStyle(tempDate)
|
||||
})
|
||||
const getValue = (date: Date | null | undefined, id: TEdits): number => {
|
||||
if (date == undefined) date = today
|
||||
switch (id) {
|
||||
case 'day': return date.getDate()
|
||||
case 'month': return date.getMonth() + 1
|
||||
case 'year': return date.getFullYear()
|
||||
case 'hour': return date.getHours()
|
||||
case 'min': return date.getMinutes()
|
||||
}
|
||||
}
|
||||
const setValue = (val: number, date: Date, id: TEdits): Date => {
|
||||
switch (id) {
|
||||
case 'day':
|
||||
date.setDate(val)
|
||||
break
|
||||
case 'month':
|
||||
date.setMonth(val - 1)
|
||||
break
|
||||
case 'year':
|
||||
date.setFullYear(val)
|
||||
break
|
||||
case 'hour':
|
||||
date.setHours(val)
|
||||
break
|
||||
case 'min':
|
||||
date.setMinutes(val)
|
||||
break
|
||||
}
|
||||
return date
|
||||
}
|
||||
const getMaxValue = (date: Date, id: TEdits): number => {
|
||||
switch (id) {
|
||||
case 'day': return daysInMonth(date)
|
||||
case 'month': return 12
|
||||
case 'year': return 3000
|
||||
case 'hour': return 23
|
||||
case 'min': return 59
|
||||
}
|
||||
days = days
|
||||
}
|
||||
$: if (currentDate) renderCellStyles()
|
||||
|
||||
const scrolling = (ev: Event): void => {
|
||||
// console.log('!!! Scrolling:', ev)
|
||||
const dateToEdits = (): void => {
|
||||
edits.forEach(edit => {
|
||||
edit.value = getValue(currentDate, edit.id)
|
||||
})
|
||||
edits = edits
|
||||
}
|
||||
const clearEdits = (): void => {
|
||||
edits.forEach(edit => edit.value = -1)
|
||||
if (edits[0].el) edits[0].el.focus()
|
||||
}
|
||||
const fixEdits = (): void => {
|
||||
const h: number = edits[3].value === -1 ? 0 : edits[3].value
|
||||
const m: number = edits[4].value === -1 ? 0 : edits[4].value
|
||||
viewDate = currentDate = new Date(edits[2].value, edits[1].value - 1, edits[0].value, h, m)
|
||||
}
|
||||
const isNull = (full: boolean = false): boolean => {
|
||||
let result: boolean = false
|
||||
edits.forEach((edit, i) => {
|
||||
if (edit.value === -1 && full && i > 2) result = true
|
||||
if (edit.value === -1 && !full && i < 3) result = true
|
||||
if (i === 0 && edit.value === 0) result = true
|
||||
if (i === 2 && (edit.value < 1970 || edit.value > 3000)) result = true
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
const saveDate = (): void => {
|
||||
if (currentDate) {
|
||||
currentDate.setHours(edits[3].value > 0 ? edits[3].value : 0)
|
||||
currentDate.setMinutes(edits[4].value > 0 ? edits[4].value : 0)
|
||||
currentDate.setSeconds(0, 0)
|
||||
viewDate = currentDate = currentDate
|
||||
dateToEdits()
|
||||
dispatch('update', currentDate)
|
||||
}
|
||||
}
|
||||
const closeDP = (): void => {
|
||||
if (!isNull()) saveDate()
|
||||
else {
|
||||
currentDate = null
|
||||
dispatch('update', null)
|
||||
}
|
||||
dispatch('close')
|
||||
}
|
||||
|
||||
const keyDown = (ev: KeyboardEvent, ed: TEdits): void => {
|
||||
if (selected === ed) {
|
||||
const index = getIndex(ed)
|
||||
if (ev.key >= '0' && ev.key <= '9') {
|
||||
const num: number = parseInt(ev.key, 10)
|
||||
if (startTyping) {
|
||||
if (num === 0) edits[index].value = 0
|
||||
else {
|
||||
edits[index].value = num
|
||||
startTyping = false
|
||||
}
|
||||
} else if (edits[index].value * 10 + num > getMaxValue(viewDate, ed)) {
|
||||
edits[index].value = getMaxValue(viewDate, ed)
|
||||
} else {
|
||||
edits[index].value = edits[index].value * 10 + num
|
||||
}
|
||||
if (!isNull(false) && !startTyping) {
|
||||
fixEdits()
|
||||
currentDate = setValue(edits[index].value, viewDate, ed)
|
||||
dateToEdits()
|
||||
}
|
||||
edits = edits
|
||||
|
||||
if (selected === 'day' && edits[0].value > getMaxValue(viewDate, 'day') / 10) selected = 'month'
|
||||
else if (selected === 'month' && edits[1].value > 1) selected = 'year'
|
||||
else if (selected === 'year' && withTime && edits[2].value > 999) selected = 'hour'
|
||||
else if (selected === 'hour' && edits[3].value > 2) selected = 'min'
|
||||
}
|
||||
if (ev.code === 'Enter') {
|
||||
if (!isNull(false)) closeDP()
|
||||
}
|
||||
if (ev.code === 'Backspace') {
|
||||
edits[index].value = -1
|
||||
startTyping = true
|
||||
}
|
||||
if (ev.code === 'ArrowUp' || ev.code === 'ArrowDown' && edits[index].el) {
|
||||
if (edits[index].value !== -1) {
|
||||
let val = (ev.code === 'ArrowUp')
|
||||
? edits[index].value + 1
|
||||
: edits[index].value - 1
|
||||
if (currentDate) {
|
||||
currentDate = setValue(val, currentDate, ed)
|
||||
dateToEdits()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ev.code === 'ArrowLeft' && edits[index].el) {
|
||||
selected = index === 0 ? edits[withTime ? 4 : 2].id : edits[index - 1].id
|
||||
}
|
||||
if (ev.code === 'ArrowRight' && edits[index].el) {
|
||||
selected = index === (withTime ? 4 : 2) ? edits[0].id : edits[index + 1].id
|
||||
}
|
||||
if (ev.code === 'Tab') {
|
||||
if ((ed === 'year' && !withTime) || (ed === 'min' && withTime)) saveDate()
|
||||
}
|
||||
}
|
||||
}
|
||||
const focused = (ed: TEdits): void => {
|
||||
selected = ed
|
||||
startTyping = true
|
||||
}
|
||||
const updateDate = (date: Date | null): void => {
|
||||
if (date != undefined) {
|
||||
currentDate = date
|
||||
dateToEdits()
|
||||
closeDP()
|
||||
}
|
||||
}
|
||||
const navigateMonth = (result: any): void => {
|
||||
if (result != undefined) {
|
||||
if (result.charAt(1) === 'm') viewDate.setMonth(viewDate.getMonth() + (result === '-m' ? -1 : 1))
|
||||
viewDate = viewDate
|
||||
}
|
||||
}
|
||||
const changeMonth = (date: Date, up: boolean): Date => {
|
||||
return new Date(date.getFullYear(), date.getMonth() + (up ? 1 : -1), date.getDate())
|
||||
}
|
||||
|
||||
if (currentDate != undefined) dateToEdits()
|
||||
$: if (selected && edits[getIndex(selected)].el) edits[getIndex(selected)].el?.focus()
|
||||
$: if (viewDate) viewDateSec = changeMonth(viewDate, true)
|
||||
|
||||
afterUpdate(() => {
|
||||
if (value) currentDate = new Date(value)
|
||||
})
|
||||
onMount(() => {
|
||||
if (scrollDiv) scrollDiv.addEventListener('wheel', scrolling)
|
||||
})
|
||||
onDestroy(() => {
|
||||
if (scrollDiv) scrollDiv.removeEventListener('wheel', scrolling)
|
||||
if (selected != undefined) edits[getIndex(selected)].el?.focus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<div bind:this={scrollDiv} class="convert-scroller">
|
||||
<div class="popup">
|
||||
<div class="flex-center monthYear">
|
||||
<div class="date-popup-container">
|
||||
<div class="header">
|
||||
<span class="fs-title overflow-label"><Label label={popupCaption} /></span>
|
||||
<ActionIcon icon={IconClose} size={'small'} action={() => { dispatch('close') }} />
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="label">
|
||||
<span class="bold"><Label label={ui.string.DueDate} /></span>
|
||||
<span class="divider">-</span>
|
||||
<Label label={ui.string.IssueNeedsToBeCompletedByThisDate} />
|
||||
</div>
|
||||
|
||||
<div class="datetime-input">
|
||||
<div class="flex-row-center">
|
||||
<span bind:this={edits[0].el} class="digit" tabindex="0"
|
||||
on:keydown={(ev) => keyDown(ev, edits[0].id)}
|
||||
on:focus={() => focused(edits[0].id)}
|
||||
on:blur={() => selected = null}
|
||||
>
|
||||
{#if (edits[0].value > -1)}
|
||||
{edits[0].value.toString().padStart(2, '0')}
|
||||
{:else}ДД{/if}
|
||||
</span>
|
||||
<span class="separator">.</span>
|
||||
<span bind:this={edits[1].el} class="digit" tabindex="0"
|
||||
on:keydown={(ev) => keyDown(ev, edits[1].id)}
|
||||
on:focus={() => focused(edits[1].id)}
|
||||
on:blur={() => selected = null}
|
||||
>
|
||||
{#if (edits[1].value > -1)}
|
||||
{edits[1].value.toString().padStart(2, '0')}
|
||||
{:else}ММ{/if}
|
||||
</span>
|
||||
<span class="separator">.</span>
|
||||
<span bind:this={edits[2].el} class="digit" tabindex="0"
|
||||
on:keydown={(ev) => keyDown(ev, edits[2].id)}
|
||||
on:focus={() => focused(edits[2].id)}
|
||||
on:blur={() => selected = null}
|
||||
>
|
||||
{#if (edits[2].value > -1)}
|
||||
{edits[2].value.toString().padStart(4, '0')}
|
||||
{:else}ГГГГ{/if}
|
||||
</span>
|
||||
{#if withTime}
|
||||
<div class="time-divider" />
|
||||
<span bind:this={edits[3].el} class="digit" tabindex="0"
|
||||
on:keydown={(ev) => keyDown(ev, edits[3].id)}
|
||||
on:focus={() => focused(edits[3].id)}
|
||||
on:blur={() => selected = null}
|
||||
>
|
||||
{#if (edits[3].value > -1)}
|
||||
{edits[3].value.toString().padStart(2, '0')}
|
||||
{:else}ЧЧ{/if}
|
||||
</span>
|
||||
<span class="separator">:</span>
|
||||
<span bind:this={edits[4].el} class="digit" tabindex="0"
|
||||
on:keydown={(ev) => keyDown(ev, edits[4].id)}
|
||||
on:focus={() => focused(edits[4].id)}
|
||||
on:blur={() => selected = null}
|
||||
>
|
||||
{#if (edits[4].value > -1)}
|
||||
{edits[4].value.toString().padStart(2, '0')}
|
||||
{:else}ММ{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if currentDate}
|
||||
{getMonthName(currentDate)}
|
||||
<span class="ml-1">{currentDate.getFullYear()}</span>
|
||||
<div
|
||||
class="close-btn" tabindex="0"
|
||||
on:click={() => {
|
||||
selected = 'day'
|
||||
startTyping = true
|
||||
currentDate = null
|
||||
clearEdits()
|
||||
}}
|
||||
on:blur={() => selected = null}
|
||||
>
|
||||
<Icon icon={IconClose} size={'x-small'} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if currentDate}
|
||||
<div class="calendar" class:no-editable={!editable}>
|
||||
{#each [...Array(7).keys()] as dayOfWeek}
|
||||
<div class="caption">{getWeekDayName(day(firstDayOfCurrentMonth, dayOfWeek), 'short')}</div>
|
||||
{/each}
|
||||
{#each days as day, i}
|
||||
<div
|
||||
class="day {day.style}"
|
||||
style="grid-column: {day.dayOfWeek}/{day.dayOfWeek + 1};"
|
||||
on:click|stopPropagation={() => {
|
||||
if (currentDate) currentDate.setDate(i + 1)
|
||||
value = currentDate.getTime()
|
||||
dispatch('update', value)
|
||||
}}
|
||||
>
|
||||
{i + 1}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="month-group">
|
||||
<MonthSquare
|
||||
bind:currentDate={currentDate}
|
||||
{viewDate}
|
||||
{mondayStart}
|
||||
viewUpdate={false}
|
||||
hideNavigator
|
||||
on:update={(result) => updateDate(result.detail)}
|
||||
/>
|
||||
<MonthSquare
|
||||
bind:currentDate={currentDate}
|
||||
viewDate={viewDateSec}
|
||||
{mondayStart}
|
||||
viewUpdate={false}
|
||||
on:update={(result) => updateDate(result.detail)}
|
||||
on:navigation={(result) => navigateMonth(result.detail)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<Button kind={'primary'} label={ui.string.SaveDueDate} size={'x-large'} width={'100%'} on:click={closeDP} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.convert-scroller {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: .5rem;
|
||||
user-select: none;
|
||||
|
||||
overflow-x: scroll;
|
||||
overflow-y: scroll;
|
||||
// width: calc(100% - 1px);
|
||||
// max-height: calc(100% - 1px);
|
||||
// mask-image: linear-gradient(0deg, rgba(0, 0, 0, 0) 0, rgba(0, 0, 0, 1) 2rem, rgba(0, 0, 0, 1) calc(100% - 2rem), rgba(0, 0, 0, 0) 100%);
|
||||
&::-webkit-scrollbar:vertical { width: 0; }
|
||||
&::-webkit-scrollbar:horizontal { height: 0; }
|
||||
}
|
||||
|
||||
.popup {
|
||||
.date-popup-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
// width: calc(100% + 1px);
|
||||
// height: calc(100% + 1px);
|
||||
max-width: calc(100vw - 2rem);
|
||||
max-height: calc(100vh - 2rem);
|
||||
width: max-content;
|
||||
height: max-content;
|
||||
color: var(--theme-caption-color);
|
||||
background: var(--board-card-bg-color);
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: .5rem;
|
||||
// pointer-events: none;
|
||||
box-shadow: var(--card-shadow);
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 1.5rem 1rem 2rem;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1.5rem 2rem;
|
||||
min-height: 0;
|
||||
|
||||
.label {
|
||||
padding-left: 2px;
|
||||
margin-bottom: .25rem;
|
||||
font-size: .8125rem;
|
||||
color: var(--content-color);
|
||||
|
||||
.bold {
|
||||
font-weight: 500;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
.divider {
|
||||
margin: 0 .25rem;
|
||||
line-height: 1.4375rem;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
}
|
||||
|
||||
.month-group {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
margin: .5rem -.5rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 1rem 2rem;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
}
|
||||
|
||||
.monthYear {
|
||||
margin: 0 1rem;
|
||||
// line-height: 150%;
|
||||
color: var(--theme-content-accent-color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.datetime-input {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
margin: 0;
|
||||
padding: .75rem;
|
||||
height: 3rem;
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
color: var(--content-color);
|
||||
background-color: var(--body-color);
|
||||
border: 1px solid var(--button-border-color);
|
||||
border-radius: .25rem;
|
||||
transition: border-color .15s ease;
|
||||
|
||||
.calendar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: .125rem;
|
||||
&:hover { border-color: var(--button-border-hover); }
|
||||
&:focus-within {
|
||||
color: var(--caption-color);
|
||||
border-color: var(--primary-edit-border-color);
|
||||
}
|
||||
|
||||
.caption, .day {
|
||||
.close-btn {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
color: var(--theme-content-dark-color);
|
||||
}
|
||||
.caption {
|
||||
font-size: .75rem;
|
||||
color: var(--theme-content-trans-color);
|
||||
}
|
||||
.day {
|
||||
background-color: rgba(var(--theme-caption-color), .05);
|
||||
border: 1px solid transparent;
|
||||
border-radius: .25rem;
|
||||
margin: 0 .25rem;
|
||||
width: .75rem;
|
||||
height: .75rem;
|
||||
color: var(--content-color);
|
||||
background-color: var(--button-bg-color);
|
||||
outline: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
|
||||
&.selected {
|
||||
background-color: var(--primary-button-enabled);
|
||||
border-color: var(--primary-button-focused-border);
|
||||
color: var(--primary-button-color);
|
||||
&:hover {
|
||||
color: var(--accent-color);
|
||||
background-color: var(--button-bg-hover);
|
||||
}
|
||||
&.today {
|
||||
position: relative;
|
||||
border-color: var(--theme-content-color);
|
||||
font-weight: 500;
|
||||
color: var(--theme-caption-color);
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
content: attr(data-today);
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-weight: 600;
|
||||
font-size: .35rem;
|
||||
text-transform: uppercase;
|
||||
color: var(--theme-content-dark-color);
|
||||
}
|
||||
}
|
||||
&.focused { box-shadow: 0 0 0 3px var(--primary-button-outline); }
|
||||
}
|
||||
|
||||
// &.no-editable { pointer-events: none; }
|
||||
.digit {
|
||||
position: relative;
|
||||
padding: 0 .125rem;
|
||||
height: 1.5rem;
|
||||
line-height: 1.5rem;
|
||||
color: var(--accent-color);
|
||||
outline: none;
|
||||
border-radius: .125rem;
|
||||
|
||||
&:focus { background-color: var(--primary-bg-color); }
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 11000;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.time-divider {
|
||||
flex-shrink: 0;
|
||||
margin: 0 .25rem;
|
||||
width: 1px;
|
||||
min-width: 1px;
|
||||
height: .75rem;
|
||||
background-color: var(--button-border-color);
|
||||
}
|
||||
.separator { margin: 0 .1rem; }
|
||||
}
|
||||
</style>
|
||||
|
@ -13,567 +13,152 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { translate } from '@anticrm/platform'
|
||||
import { onMount, createEventDispatcher, afterUpdate, onDestroy } from 'svelte'
|
||||
import { firstDay, daysInMonth, getWeekDayName, getMonthName, day, areDatesEqual } from './internal/DateUtils'
|
||||
import type { TCellStyle, ICell } from './internal/DateUtils'
|
||||
import ui, { TimePopup, Icon, IconClose } from '../..'
|
||||
import DPClock from './icons/DPClock.svelte'
|
||||
import type { IntlString } from '@anticrm/platform'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { getMonthName } from './internal/DateUtils'
|
||||
import ui, { Label, Icon, showPopup } from '../..'
|
||||
import DPCalendar from './icons/DPCalendar.svelte'
|
||||
import DPCalendarOver from './icons/DPCalendarOver.svelte'
|
||||
import DatePopup from './DatePopup.svelte'
|
||||
|
||||
export let value: number | null | undefined
|
||||
export let withTime: boolean = false
|
||||
export let mondayStart: boolean = true
|
||||
export let editable: boolean = false
|
||||
export let icon: 'normal' | 'warning' | 'overdue' = 'normal'
|
||||
export let labelOver: IntlString | undefined = undefined // label instead of date
|
||||
export let labelNull: IntlString = ui.string.NoDate
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
type TEdits = 'day' | 'month' | 'year' | 'hour' | 'min'
|
||||
interface IEdits {
|
||||
id: TEdits
|
||||
value: number
|
||||
el?: HTMLElement
|
||||
}
|
||||
const editsType: TEdits[] = ['day', 'month', 'year', 'hour', 'min']
|
||||
const getIndex = (id: TEdits): number => editsType.indexOf(id)
|
||||
const today: Date = new Date(Date.now())
|
||||
$: firstDayOfCurrentMonth = firstDay(currentDate, mondayStart)
|
||||
let currentDate: Date = new Date(value ?? Date.now())
|
||||
currentDate.setSeconds(0, 0)
|
||||
let selected: TEdits = 'day'
|
||||
let todayString: string
|
||||
translate(ui.string.Today, {}).then(res => todayString = res)
|
||||
let currentDate: Date | null = null
|
||||
if (value != undefined) currentDate = new Date(value)
|
||||
let opened: boolean = false
|
||||
|
||||
let edit: boolean = false
|
||||
let startTyping: boolean = false
|
||||
let datePresenter: HTMLElement
|
||||
let datePopup: HTMLElement
|
||||
let closeBtn: HTMLElement
|
||||
|
||||
let days: Array<ICell> = []
|
||||
let edits: IEdits[] = [{ id: 'day', value: -1 }, { id: 'month', value: -1 }, { id: 'year', value: -1 },
|
||||
{ id: 'hour', value: -1 }, { id: 'min', value: -1 }]
|
||||
|
||||
const getDateStyle = (date: Date): TCellStyle => {
|
||||
if (value !== undefined && value !== null && areDatesEqual(currentDate, date)) return 'selected'
|
||||
return 'not-selected'
|
||||
}
|
||||
const renderCellStyles = (): void => {
|
||||
days = []
|
||||
for (let i = 1; i <= daysInMonth(currentDate); i++) {
|
||||
const tempDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), i)
|
||||
days.push({
|
||||
dayOfWeek: (tempDate.getDay() === 0) ? 7 : tempDate.getDay(),
|
||||
style: getDateStyle(tempDate)
|
||||
})
|
||||
}
|
||||
days = days
|
||||
}
|
||||
$: if (currentDate) renderCellStyles()
|
||||
|
||||
const getValue = (date: Date | null | undefined = today, id: TEdits): number => {
|
||||
switch (id) {
|
||||
case 'day': return date ? date.getDate() : today.getDate()
|
||||
case 'month': return date ? date.getMonth() + 1 : today.getMonth() + 1
|
||||
case 'year': return date ? date.getFullYear() : today.getFullYear()
|
||||
case 'hour': return date ? date.getHours() : today.getHours()
|
||||
case 'min': return date ? date.getMinutes() : today.getMinutes()
|
||||
}
|
||||
}
|
||||
const setValue = (val: number, date: Date, id: TEdits): Date => {
|
||||
switch (id) {
|
||||
case 'day':
|
||||
date.setDate(val)
|
||||
break
|
||||
case 'month':
|
||||
date.setMonth(val - 1)
|
||||
break
|
||||
case 'year':
|
||||
date.setFullYear(val)
|
||||
break
|
||||
case 'hour':
|
||||
date.setHours(val)
|
||||
break
|
||||
case 'min':
|
||||
date.setMinutes(val)
|
||||
break
|
||||
}
|
||||
return date
|
||||
}
|
||||
const getMaxValue = (date: Date, id: TEdits): number => {
|
||||
switch (id) {
|
||||
case 'day': return daysInMonth(date)
|
||||
case 'month': return 12
|
||||
case 'year': return 3000
|
||||
case 'hour': return 23
|
||||
case 'min': return 59
|
||||
}
|
||||
}
|
||||
|
||||
const dateToEdits = (): void => {
|
||||
edits.forEach(edit => {
|
||||
edit.value = getValue(currentDate, edit.id)
|
||||
})
|
||||
edits = edits
|
||||
}
|
||||
const saveDate = (): void => {
|
||||
currentDate.setHours(edits[3].value > 0 ? edits[3].value : 0)
|
||||
currentDate.setMinutes(edits[4].value > 0 ? edits[4].value : 0)
|
||||
currentDate.setSeconds(0, 0)
|
||||
value = currentDate.getTime()
|
||||
dateToEdits()
|
||||
renderCellStyles()
|
||||
dispatch('change', value)
|
||||
}
|
||||
if (value !== null && value !== undefined) dateToEdits()
|
||||
else if (value === null) {
|
||||
edits.map(edit => edit.value = -1)
|
||||
currentDate = today
|
||||
}
|
||||
|
||||
const fixEdits = (): void => {
|
||||
let tempValues: number[] = []
|
||||
edits.forEach((edit, i) => {
|
||||
tempValues[i] = edit.value > 0 ? edit.value : getValue(currentDate, edit.id)
|
||||
})
|
||||
currentDate = new Date(tempValues[2], tempValues[1] - 1, tempValues[0], tempValues[3], tempValues[4])
|
||||
}
|
||||
const isNull = (full: boolean = false): boolean => {
|
||||
let result: boolean = false
|
||||
edits.forEach((edit, i) => {
|
||||
if (edit.value < 1 && full) result = true
|
||||
if (i < 3 && !full && edit.value < 1) result = true
|
||||
})
|
||||
return result
|
||||
}
|
||||
const closeDatePopup = (): void => {
|
||||
if (!isNull()) saveDate()
|
||||
else {
|
||||
const onChange = (result: Date | null): void => {
|
||||
if (result === null) {
|
||||
currentDate = null
|
||||
value = null
|
||||
dispatch('change', null)
|
||||
} else {
|
||||
currentDate = result
|
||||
value = currentDate.getTime()
|
||||
}
|
||||
edit = false
|
||||
}
|
||||
|
||||
const keyDown = (ev: KeyboardEvent, ed: TEdits): void => {
|
||||
const target = ev.target as HTMLElement
|
||||
const index = getIndex(ed)
|
||||
|
||||
if (ev.key >= '0' && ev.key <= '9') {
|
||||
const num: number = parseInt(ev.key, 10)
|
||||
|
||||
if (startTyping) {
|
||||
if (num === 0) edits[index].value = 0
|
||||
else {
|
||||
edits[index].value = num
|
||||
startTyping = false
|
||||
}
|
||||
} else if (edits[index].value * 10 + num > getMaxValue(currentDate, ed)) {
|
||||
edits[index].value = getMaxValue(currentDate, ed)
|
||||
} else {
|
||||
edits[index].value = edits[index].value * 10 + num
|
||||
}
|
||||
|
||||
if (!isNull(false) && edits[2].value > 999 && !startTyping) {
|
||||
fixEdits()
|
||||
currentDate = setValue(edits[index].value, currentDate, ed)
|
||||
dateToEdits()
|
||||
}
|
||||
edits = edits
|
||||
|
||||
if (selected === 'day' && edits[0].value > getMaxValue(currentDate, 'day') / 10) selected = 'month'
|
||||
else if (selected === 'month' && edits[1].value > 1) selected = 'year'
|
||||
else if (selected === 'year' && withTime && edits[2].value > 999) selected = 'hour'
|
||||
else if (selected === 'hour' && edits[3].value > 2) selected = 'min'
|
||||
}
|
||||
if (ev.code === 'Enter') closeDatePopup()
|
||||
if (ev.code === 'Backspace') {
|
||||
edits[index].value = -1
|
||||
startTyping = true
|
||||
}
|
||||
if (ev.code === 'ArrowUp' || ev.code === 'ArrowDown' && edits[index].el) {
|
||||
if (edits[index].value !== -1) {
|
||||
let val = (ev.code === 'ArrowUp')
|
||||
? edits[index].value + 1
|
||||
: edits[index].value - 1
|
||||
if (currentDate) {
|
||||
currentDate = setValue(val, currentDate, ed)
|
||||
dateToEdits()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ev.code === 'ArrowLeft' && edits[index].el) {
|
||||
selected = index === 0 ? edits[withTime ? 4 : 2].id : edits[index - 1].id
|
||||
}
|
||||
if (ev.code === 'ArrowRight' && edits[index].el) {
|
||||
selected = index === (withTime ? 4 : 2) ? edits[0].id : edits[index + 1].id
|
||||
}
|
||||
if (ev.code === 'Tab') {
|
||||
if ((ed === 'year' && !withTime) || (ed === 'min' && withTime)) closeDatePopup()
|
||||
}
|
||||
}
|
||||
|
||||
const focused = (ed: TEdits): void => {
|
||||
selected = ed
|
||||
startTyping = true
|
||||
}
|
||||
const unfocus = (ev: FocusEvent, ed: TEdits | HTMLElement): void => {
|
||||
const target = ev.relatedTarget as HTMLElement
|
||||
let kl: boolean = false
|
||||
edits.forEach(edit => { if (edit.el === target) kl = true })
|
||||
if (target === datePopup || target === closeBtn) kl = true
|
||||
if (!kl || target === null) closeDatePopup()
|
||||
}
|
||||
|
||||
$: if (selected && edits[getIndex(selected)].el) edits[getIndex(selected)].el?.focus()
|
||||
afterUpdate(() => {
|
||||
const tempEl = edits[getIndex(selected)].el
|
||||
if (tempEl) tempEl.focus()
|
||||
})
|
||||
|
||||
const fitPopup = (): void => {
|
||||
if (datePresenter && datePopup) {
|
||||
const rect: DOMRect = datePresenter.getBoundingClientRect()
|
||||
const rectPopup: DOMRect = datePopup.getBoundingClientRect()
|
||||
if (document.body.clientHeight - rect.bottom < rect.top) { // Top
|
||||
datePopup.style.top = 'none'
|
||||
datePopup.style.bottom = rect.top - 4 + 'px'
|
||||
} else { // Bottom
|
||||
datePopup.style.bottom = 'none'
|
||||
datePopup.style.top = rect.bottom + 4 + 'px'
|
||||
}
|
||||
datePopup.style.left = (rect.left + rect.width / 2) - datePopup.clientWidth / 2 + 'px'
|
||||
console.log('!!!!!!! Popup - rect:', rect, ' --- rectPopup:', rectPopup, ' --- afterPopup:', datePopup.getBoundingClientRect())
|
||||
}
|
||||
}
|
||||
$: if (edit) {
|
||||
if (datePopup) {
|
||||
fitPopup()
|
||||
datePopup.style.visibility = 'visible'
|
||||
}
|
||||
} else {
|
||||
if (datePopup) datePopup.style.visibility = 'hidden'
|
||||
value = value
|
||||
dispatch('change', value)
|
||||
opened = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
bind:this={datePresenter}
|
||||
class="button normal"
|
||||
class:edit
|
||||
class="datetime-button"
|
||||
class:editable
|
||||
on:click={() => { if (editable) edit = true }}
|
||||
on:click={() => {
|
||||
if (editable && !opened) {
|
||||
opened = true
|
||||
showPopup(DatePopup,
|
||||
{ currentDate, mondayStart, withTime },
|
||||
undefined,
|
||||
() => { opened = false },
|
||||
(result) => { if (result !== undefined) onChange(result) })
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if edit}
|
||||
<span bind:this={edits[0].el} class="digit" tabindex="0"
|
||||
on:keydown={(ev) => keyDown(ev, edits[0].id)}
|
||||
on:focus={() => focused(edits[0].id)}
|
||||
on:blur={(ev) => unfocus(ev, edits[0].id)}
|
||||
>
|
||||
{#if (value !== null && value !== undefined) || (edits[0].value > 0)}
|
||||
{edits[0].value.toString().padStart(2, '0')}
|
||||
{:else}ДД{/if}
|
||||
</span>
|
||||
<span class="separator">.</span>
|
||||
<span bind:this={edits[1].el} class="digit" tabindex="0"
|
||||
on:keydown={(ev) => keyDown(ev, edits[1].id)}
|
||||
on:focus={() => focused(edits[1].id)}
|
||||
on:blur={(ev) => unfocus(ev, edits[1].id)}
|
||||
>
|
||||
{#if (value !== null && value !== undefined) || (edits[1].value > 0)}
|
||||
{edits[1].value.toString().padStart(2, '0')}
|
||||
{:else}ММ{/if}
|
||||
</span>
|
||||
<span class="separator">.</span>
|
||||
<span bind:this={edits[2].el} class="digit" tabindex="0"
|
||||
on:keydown={(ev) => keyDown(ev, edits[2].id)}
|
||||
on:focus={() => focused(edits[2].id)}
|
||||
on:blur={(ev) => unfocus(ev, edits[2].id)}
|
||||
>
|
||||
{#if (value !== null && value !== undefined) || (edits[2].value > 0)}
|
||||
{edits[2].value.toString().padStart(4, '0')}
|
||||
{:else}ГГГГ{/if}
|
||||
</span>
|
||||
{#if withTime}
|
||||
<div class="time-divider" />
|
||||
<span bind:this={edits[3].el} class="digit" tabindex="0"
|
||||
on:keydown={(ev) => keyDown(ev, edits[3].id)}
|
||||
on:focus={() => focused(edits[3].id)}
|
||||
on:blur={(ev) => unfocus(ev, edits[3].id)}
|
||||
>
|
||||
{#if (value !== null && value !== undefined) || (edits[3].value > -1)}
|
||||
{edits[3].value.toString().padStart(2, '0')}
|
||||
{:else}ЧЧ{/if}
|
||||
</span>
|
||||
<span class="separator">:</span>
|
||||
<span bind:this={edits[4].el} class="digit" tabindex="0"
|
||||
on:keydown={(ev) => keyDown(ev, edits[4].id)}
|
||||
on:focus={() => focused(edits[4].id)}
|
||||
on:blur={(ev) => unfocus(ev, edits[4].id)}
|
||||
>
|
||||
{#if (value !== null && value !== undefined) || (edits[4].value > -1)}
|
||||
{edits[4].value.toString().padStart(2, '0')}
|
||||
{:else}ММ{/if}
|
||||
</span>
|
||||
{/if}
|
||||
{#if value}
|
||||
<div
|
||||
bind:this={closeBtn} class="close-btn" tabindex="0"
|
||||
on:click={() => {
|
||||
selected = 'day'
|
||||
startTyping = true
|
||||
value = null
|
||||
edits.forEach(edit => edit.value = -1)
|
||||
if (edits[0].el) edits[0].el.focus()
|
||||
}}
|
||||
on:blur={(ev) => unfocus(ev, closeBtn)}
|
||||
>
|
||||
<Icon icon={IconClose} size={'x-small'} />
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="btn-icon mr-1">
|
||||
<Icon icon={DPCalendar} size={'small'}/>
|
||||
</div>
|
||||
{#if value !== null && value !== undefined}
|
||||
<span class="digit">{new Date(value).getDate()}</span>
|
||||
<span class="digit">{getMonthName(new Date(value), 'short')}</span>
|
||||
<div class="btn-icon {icon}">
|
||||
<Icon icon={icon === 'overdue' ? DPCalendarOver : DPCalendar} size={'full'}/>
|
||||
</div>
|
||||
{#if value !== null && value !== undefined}
|
||||
{#if labelOver !== undefined}
|
||||
<Label label={labelOver} />
|
||||
{:else}
|
||||
{new Date(value).getDate()} {getMonthName(new Date(value), 'short')}
|
||||
{#if new Date(value).getFullYear() !== today.getFullYear()}
|
||||
<span class="digit">{new Date(value).getFullYear()}</span>
|
||||
{new Date(value).getFullYear()}
|
||||
{/if}
|
||||
{#if withTime}
|
||||
<div class="time-divider" />
|
||||
<span class="digit">{new Date(value).getHours().toString().padStart(2, '0')}</span>
|
||||
{new Date(value).getHours().toString().padStart(2, '0')}
|
||||
<span class="separator">:</span>
|
||||
<span class="digit">{new Date(value).getMinutes().toString().padStart(2, '0')}</span>
|
||||
{new Date(value).getMinutes().toString().padStart(2, '0')}
|
||||
{/if}
|
||||
{:else}
|
||||
No date
|
||||
{/if}
|
||||
{:else}
|
||||
<Label label={labelNull} />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<div bind:this={datePopup} class="datetime-popup-container" tabindex="0" on:blur={(ev) => unfocus(ev, datePopup)}>
|
||||
<div class="popup antiCard">
|
||||
<div class="flex-center monthYear">
|
||||
{#if currentDate}
|
||||
{getMonthName(currentDate)}
|
||||
<span class="ml-1">{currentDate.getFullYear()}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if currentDate}
|
||||
<div class="calendar" class:no-editable={!editable}>
|
||||
{#each [...Array(7).keys()] as dayOfWeek}
|
||||
<div class="caption">{getWeekDayName(day(firstDayOfCurrentMonth, dayOfWeek), 'short')}</div>
|
||||
{/each}
|
||||
{#each days as day, i}
|
||||
<div
|
||||
class="day {day.style}"
|
||||
style="grid-column: {day.dayOfWeek}/{day.dayOfWeek + 1};"
|
||||
on:click|stopPropagation={() => {
|
||||
if (currentDate) currentDate.setDate(i + 1)
|
||||
saveDate()
|
||||
}}
|
||||
>
|
||||
{i + 1}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.datetime-popup-container {
|
||||
visibility: hidden;
|
||||
position: fixed;
|
||||
outline: none;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.popup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
|
||||
.monthYear {
|
||||
margin: 0 1rem;
|
||||
color: var(--theme-content-accent-color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.calendar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1.75rem);
|
||||
gap: .125rem;
|
||||
|
||||
.caption, .day {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
color: var(--theme-content-dark-color);
|
||||
}
|
||||
.caption {
|
||||
font-size: .75rem;
|
||||
color: var(--theme-content-trans-color);
|
||||
}
|
||||
.day {
|
||||
background-color: rgba(var(--theme-caption-color), .05);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
|
||||
&.selected {
|
||||
background-color: var(--primary-button-enabled);
|
||||
border-color: var(--primary-button-focused-border);
|
||||
color: var(--primary-button-color);
|
||||
}
|
||||
&.today {
|
||||
position: relative;
|
||||
border-color: var(--theme-content-color);
|
||||
font-weight: 500;
|
||||
color: var(--theme-caption-color);
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
content: attr(data-today);
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-weight: 600;
|
||||
font-size: .35rem;
|
||||
text-transform: uppercase;
|
||||
color: var(--theme-content-dark-color);
|
||||
}
|
||||
}
|
||||
&.focused { box-shadow: 0 0 0 3px var(--primary-button-outline); }
|
||||
}
|
||||
|
||||
// &.no-editable { pointer-events: none; }
|
||||
}
|
||||
|
||||
.button {
|
||||
.datetime-button {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
padding: 0 .5rem;
|
||||
font-weight: 500;
|
||||
font-weight: 400;
|
||||
min-width: 1.5rem;
|
||||
width: auto;
|
||||
height: 1.5rem;
|
||||
white-space: nowrap;
|
||||
line-height: 1.5rem;
|
||||
color: var(--accent-color);
|
||||
background-color: transparent;
|
||||
background-color: var(--button-bg-color);
|
||||
border: 1px solid transparent;
|
||||
border-radius: .25rem;
|
||||
box-shadow: var(--button-shadow);
|
||||
transition-property: border, background-color, color, box-shadow;
|
||||
transition-duration: .15s;
|
||||
cursor: default;
|
||||
|
||||
.btn-icon {
|
||||
color: var(--content-color);
|
||||
margin-right: .375rem;
|
||||
width: .875rem;
|
||||
height: .875rem;
|
||||
transition: color .15s;
|
||||
pointer-events: none;
|
||||
|
||||
&.normal { color: var(--content-color); }
|
||||
&.warning { color: var(--warning-color); }
|
||||
&.overdue { color: var(--error-color); }
|
||||
}
|
||||
&:hover { transition-duration: 0; }
|
||||
&:focus-within .datepopup-container { visibility: visible; }
|
||||
|
||||
&.normal {
|
||||
font-weight: 400;
|
||||
color: var(--content-color);
|
||||
background-color: var(--button-bg-color);
|
||||
box-shadow: var(--button-shadow);
|
||||
&:hover {
|
||||
color: var(--caption-color);
|
||||
transition-duration: 0;
|
||||
}
|
||||
&.editable {
|
||||
cursor: pointer;
|
||||
|
||||
&.editable {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: var(--accent-color);
|
||||
background-color: var(--button-bg-hover);
|
||||
|
||||
.btn-icon { color: var(--accent-color); }
|
||||
.time-divider {
|
||||
background-color: var(--button-border-hover);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&:focus-within {
|
||||
.time-divider {
|
||||
background-color: var(--primary-edit-border-color);
|
||||
opacity: .5;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--button-bg-hover);
|
||||
.btn-icon {
|
||||
&.normal { color: var(--caption-color); }
|
||||
&.warning { color: var(--warning-color); }
|
||||
&.overdue { color: var(--error-color); }
|
||||
}
|
||||
.time-divider { background-color: var(--button-border-hover); }
|
||||
}
|
||||
&:disabled {
|
||||
background-color: #30323655;
|
||||
cursor: default;
|
||||
&:hover {
|
||||
color: var(--content-color);
|
||||
.btn-icon { color: var(--content-color); }
|
||||
}
|
||||
&:focus-within {
|
||||
border-color: var(--primary-edit-border-color);
|
||||
&:hover { background-color: transparent; }
|
||||
}
|
||||
}
|
||||
&:disabled {
|
||||
background-color: #30323655;
|
||||
cursor: default;
|
||||
|
||||
.close-btn {
|
||||
margin: 0 .25rem;
|
||||
width: .75rem;
|
||||
height: .75rem;
|
||||
&:hover {
|
||||
color: var(--content-color);
|
||||
background-color: var(--button-bg-color);
|
||||
outline: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--accent-color);
|
||||
background-color: var(--button-bg-hover);
|
||||
}
|
||||
.btn-icon { color: var(--content-color); }
|
||||
}
|
||||
}
|
||||
&.edit {
|
||||
padding: 0 .125rem;
|
||||
background-color: transparent;
|
||||
border-color: var(--primary-edit-border-color);
|
||||
&:hover { background-color: transparent; }
|
||||
}
|
||||
|
||||
.digit {
|
||||
padding: 0 .125rem;
|
||||
height: 1.125rem;
|
||||
line-height: 1.125rem;
|
||||
color: var(--accent-color);
|
||||
outline: none;
|
||||
border-radius: .125rem;
|
||||
|
||||
&:focus { background-color: var(--primary-bg-color); }
|
||||
}
|
||||
.time-divider {
|
||||
flex-shrink: 0;
|
||||
margin: 0 .125rem;
|
||||
margin: 0 .25rem;
|
||||
width: 1px;
|
||||
min-width: 1px;
|
||||
height: .75rem;
|
||||
background-color: var(--button-border-color);
|
||||
opacity: 1;
|
||||
transition-property: opacity, background-color;
|
||||
transition-duration: .15s;
|
||||
}
|
||||
|
||||
.datepopup-container {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
top: calc(100% + .5rem);
|
||||
left: 50%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
transform: translateX(-50%);
|
||||
z-index: 10000;
|
||||
}
|
||||
.separator { margin: 0 .1rem; }
|
||||
}
|
||||
</style>
|
||||
|
@ -17,7 +17,6 @@
|
||||
import type { IntlString } from '@anticrm/platform'
|
||||
import ui, { Label } from '../..'
|
||||
import DateRangePresenter from './DateRangePresenter.svelte'
|
||||
import DateRangePopup from './DateRangePopup.svelte'
|
||||
|
||||
export let title: IntlString
|
||||
export let value: number | null | undefined = null
|
||||
|
@ -14,197 +14,28 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { dpstore, IconNavPrev, IconNavNext, Icon } from '../..'
|
||||
import { TCellStyle, ICell } from './internal/DateUtils'
|
||||
import { firstDay, day, getWeekDayName, areDatesEqual, getMonthName, daysInMonth } from './internal/DateUtils'
|
||||
import { dpstore } from '../..'
|
||||
import Month from './Month.svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let currentDate: Date
|
||||
let viewDate: Date
|
||||
$: if ($dpstore.currentDate) {
|
||||
currentDate = $dpstore.currentDate
|
||||
viewDate = new Date(currentDate)
|
||||
}
|
||||
|
||||
let mondayStart: boolean = true
|
||||
let monthYear: string
|
||||
const capitalizeFirstLetter = (str: string): string => str.charAt(0).toUpperCase() + str.slice(1)
|
||||
|
||||
$: firstDayOfCurrentMonth = firstDay(viewDate, mondayStart)
|
||||
let days: Array<ICell> = []
|
||||
|
||||
const getDateStyle = (date: Date): TCellStyle => {
|
||||
if (areDatesEqual(currentDate, date)) return 'selected'
|
||||
return 'not-selected'
|
||||
}
|
||||
|
||||
const renderCellStyles = (): void => {
|
||||
days = []
|
||||
for (let i = 1; i <= daysInMonth(viewDate); i++) {
|
||||
const tempDate = new Date(viewDate.getFullYear(), viewDate.getMonth(), i)
|
||||
days.push({
|
||||
dayOfWeek: (tempDate.getDay() === 0) ? 7 : tempDate.getDay(),
|
||||
style: getDateStyle(tempDate)
|
||||
})
|
||||
}
|
||||
days = days
|
||||
monthYear = capitalizeFirstLetter(getMonthName(viewDate)) + ' ' + viewDate.getFullYear()
|
||||
}
|
||||
$: if (viewDate) renderCellStyles()
|
||||
|
||||
const today: Date = new Date(Date.now())
|
||||
const isToday = (n: number): boolean => {
|
||||
if (areDatesEqual(today, new Date(viewDate.getFullYear(), viewDate.getMonth(), n))) return true
|
||||
return false
|
||||
}
|
||||
$: currentDate = $dpstore.currentDate ?? today
|
||||
let mondayStart: boolean = true
|
||||
</script>
|
||||
|
||||
<div class="daterange-popup-container">
|
||||
<div class="header">
|
||||
{#if viewDate}
|
||||
<div class="monthYear">{monthYear}</div>
|
||||
<div class="group">
|
||||
<div class="btn" on:click={() => {
|
||||
viewDate.setMonth(viewDate.getMonth() - 1)
|
||||
renderCellStyles()
|
||||
}}>
|
||||
<div class="icon-btn"><Icon icon={IconNavPrev} size={'full'} /></div>
|
||||
</div>
|
||||
<div class="btn" on:click={() => {
|
||||
viewDate.setMonth(viewDate.getMonth() + 1)
|
||||
renderCellStyles()
|
||||
}}>
|
||||
<div class="icon-btn"><Icon icon={IconNavNext} size={'full'} /></div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if viewDate}
|
||||
<div class="calendar">
|
||||
{#each [...Array(7).keys()] as dayOfWeek}
|
||||
<span class="caption">{capitalizeFirstLetter(getWeekDayName(day(firstDayOfCurrentMonth, dayOfWeek), 'short'))}</span>
|
||||
{/each}
|
||||
{#each days as day, i}
|
||||
<div
|
||||
class="day {day.style}"
|
||||
class:today={isToday(i)}
|
||||
class:day-off={day.dayOfWeek > 5}
|
||||
style="grid-column: {day.dayOfWeek}/{day.dayOfWeek + 1};"
|
||||
on:click|stopPropagation={() => {
|
||||
viewDate.setDate(i + 1)
|
||||
dispatch('close', viewDate)
|
||||
}}
|
||||
>
|
||||
{i + 1}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="month-popup-container">
|
||||
<Month bind:currentDate={currentDate} {mondayStart} on:update={(result) => {
|
||||
if (result.detail !== undefined) {
|
||||
dispatch('close', result.detail)
|
||||
}
|
||||
}} />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.daterange-popup-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--theme-caption-color);
|
||||
.month-popup-container {
|
||||
background: var(--popup-bg-color);
|
||||
border-radius: .5rem;
|
||||
box-shadow: var(--popup-shadow);
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 1rem .75rem;
|
||||
color: var(--caption-color);
|
||||
|
||||
.monthYear {
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
&::first-letter { text-transform: capitalize; }
|
||||
}
|
||||
.group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.btn {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 1.25rem;
|
||||
height: 1.75rem;
|
||||
color: var(--dark-color);
|
||||
cursor: pointer;
|
||||
|
||||
.icon-btn { height: .75rem; }
|
||||
&:hover { color: var(--accent-color); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.calendar {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: .5rem;
|
||||
padding: 0 1rem 1rem;
|
||||
|
||||
.caption, .day {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 1.625rem;
|
||||
height: 1.625rem;
|
||||
font-size: 1rem;
|
||||
color: var(--content-color);
|
||||
}
|
||||
.caption {
|
||||
align-items: start;
|
||||
height: 2rem;
|
||||
color: var(--dark-color);
|
||||
&::first-letter { text-transform: capitalize; }
|
||||
}
|
||||
.day {
|
||||
position: relative;
|
||||
color: var(--accent-color);
|
||||
background-color: rgba(var(--accent-color), .05);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
|
||||
&.day-off { color: var(--content-color); }
|
||||
&.today {
|
||||
font-weight: 500;
|
||||
color: var(--caption-color);
|
||||
background-color: var(--button-bg-color);
|
||||
border-color: var(--dark-color);
|
||||
}
|
||||
&.focused { box-shadow: 0 0 0 3px var(--primary-button-outline); }
|
||||
&.selected, &:hover {
|
||||
color: var(--caption-color);
|
||||
background-color: var(--primary-bg-color);
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -.625rem;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
top: 2rem;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: var(--button-bg-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -359,7 +359,6 @@
|
||||
flex-shrink: 0;
|
||||
padding: 0 .5rem;
|
||||
font-weight: 400;
|
||||
font-size: .8125rem;
|
||||
min-width: 1.5rem;
|
||||
width: auto;
|
||||
height: 1.5rem;
|
||||
@ -403,8 +402,9 @@
|
||||
.time-divider { background-color: var(--button-border-hover); }
|
||||
}
|
||||
&:focus-within {
|
||||
background-color: var(--button-bg-color);
|
||||
border-color: var(--primary-edit-border-color);
|
||||
&:hover { background-color: transparent; }
|
||||
&:hover { background-color: var(--button-bg-color); }
|
||||
}
|
||||
}
|
||||
&:disabled {
|
||||
@ -470,5 +470,6 @@
|
||||
height: .75rem;
|
||||
background-color: var(--button-border-color);
|
||||
}
|
||||
.separator { margin: 0 .1rem; }
|
||||
}
|
||||
</style>
|
||||
|
206
packages/ui/src/components/calendar/Month.svelte
Normal file
206
packages/ui/src/components/calendar/Month.svelte
Normal file
@ -0,0 +1,206 @@
|
||||
<!--
|
||||
// Copyright © 2020 Anticrm Platform Contributors.
|
||||
//
|
||||
// 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 { afterUpdate, createEventDispatcher } from 'svelte'
|
||||
import { IconNavPrev, IconNavNext, Icon } from '../..'
|
||||
import { TCellStyle, ICell } from './internal/DateUtils'
|
||||
import { firstDay, day, getWeekDayName, areDatesEqual, getMonthName, daysInMonth } from './internal/DateUtils'
|
||||
|
||||
export let currentDate: Date | null
|
||||
export let mondayStart: boolean = true
|
||||
export let hideNavigator: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let monthYear: string
|
||||
const today: Date = new Date(Date.now())
|
||||
let viewDate: Date = new Date(currentDate ?? today)
|
||||
$: firstDayOfCurrentMonth = firstDay(viewDate, mondayStart)
|
||||
const isToday = (n: number): boolean => {
|
||||
if (areDatesEqual(today, new Date(viewDate.getFullYear(), viewDate.getMonth(), n))) return true
|
||||
return false
|
||||
}
|
||||
const capitalizeFirstLetter = (str: string): string => str.charAt(0).toUpperCase() + str.slice(1)
|
||||
|
||||
let days: Array<ICell> = []
|
||||
const getDateStyle = (date: Date): TCellStyle => {
|
||||
if (currentDate != undefined && areDatesEqual(currentDate, date)) return 'selected'
|
||||
return 'not-selected'
|
||||
}
|
||||
const renderCellStyles = (): void => {
|
||||
days = []
|
||||
for (let i = 1; i <= daysInMonth(viewDate); i++) {
|
||||
const tempDate = new Date(viewDate.getFullYear(), viewDate.getMonth(), i)
|
||||
days.push({
|
||||
dayOfWeek: (tempDate.getDay() === 0) ? 7 : tempDate.getDay(),
|
||||
style: getDateStyle(tempDate)
|
||||
})
|
||||
}
|
||||
days = days
|
||||
monthYear = capitalizeFirstLetter(getMonthName(viewDate)) + ' ' + viewDate.getFullYear()
|
||||
}
|
||||
|
||||
afterUpdate(() => {
|
||||
if (viewDate) renderCellStyles()
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="month-container">
|
||||
<div class="header">
|
||||
{#if viewDate}
|
||||
<div class="monthYear">{monthYear}</div>
|
||||
<div class="group" class:hideNavigator>
|
||||
<div class="btn" on:click={() => {
|
||||
viewDate.setMonth(viewDate.getMonth() - 1)
|
||||
renderCellStyles()
|
||||
}}>
|
||||
<div class="icon-btn"><Icon icon={IconNavPrev} size={'full'} /></div>
|
||||
</div>
|
||||
<div class="btn" on:click={() => {
|
||||
viewDate.setMonth(viewDate.getMonth() + 1)
|
||||
renderCellStyles()
|
||||
}}>
|
||||
<div class="icon-btn"><Icon icon={IconNavNext} size={'full'} /></div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if viewDate}
|
||||
<div class="calendar">
|
||||
{#each [...Array(7).keys()] as dayOfWeek}
|
||||
<span class="caption">{capitalizeFirstLetter(getWeekDayName(day(firstDayOfCurrentMonth, dayOfWeek), 'short'))}</span>
|
||||
{/each}
|
||||
{#each days as day, i}
|
||||
<div
|
||||
class="day {day.style}"
|
||||
class:today={isToday(i)}
|
||||
class:day-off={day.dayOfWeek > 5}
|
||||
style="grid-column: {day.dayOfWeek}/{day.dayOfWeek + 1};"
|
||||
on:click|stopPropagation={() => {
|
||||
viewDate.setDate(i + 1)
|
||||
dispatch('update', viewDate)
|
||||
}}
|
||||
>
|
||||
{i + 1}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.month-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--theme-caption-color);
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 1rem .75rem;
|
||||
color: var(--caption-color);
|
||||
|
||||
.monthYear {
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
&::first-letter { text-transform: capitalize; }
|
||||
}
|
||||
.group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.hideNavigator { visibility: hidden; }
|
||||
.btn {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 1.25rem;
|
||||
height: 1.75rem;
|
||||
color: var(--dark-color);
|
||||
cursor: pointer;
|
||||
|
||||
.icon-btn { height: .75rem; }
|
||||
&:hover { color: var(--accent-color); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.calendar {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: .5rem;
|
||||
padding: 0 1rem 1rem;
|
||||
|
||||
.caption, .day {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 1.625rem;
|
||||
height: 1.625rem;
|
||||
font-size: 1rem;
|
||||
color: var(--content-color);
|
||||
}
|
||||
.caption {
|
||||
align-items: start;
|
||||
height: 2rem;
|
||||
color: var(--dark-color);
|
||||
&::first-letter { text-transform: capitalize; }
|
||||
}
|
||||
.day {
|
||||
position: relative;
|
||||
color: var(--accent-color);
|
||||
background-color: rgba(var(--accent-color), .05);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
|
||||
&.day-off { color: var(--content-color); }
|
||||
&.today {
|
||||
font-weight: 500;
|
||||
color: var(--caption-color);
|
||||
background-color: var(--button-bg-color);
|
||||
border-color: var(--dark-color);
|
||||
}
|
||||
&.focused { box-shadow: 0 0 0 3px var(--primary-button-outline); }
|
||||
&.selected, &:hover {
|
||||
color: var(--caption-color);
|
||||
background-color: var(--primary-bg-color);
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -.625rem;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
top: 2rem;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: var(--button-bg-color);
|
||||
}
|
||||
}
|
||||
</style>
|
201
packages/ui/src/components/calendar/MonthSquare.svelte
Normal file
201
packages/ui/src/components/calendar/MonthSquare.svelte
Normal file
@ -0,0 +1,201 @@
|
||||
<!--
|
||||
// Copyright © 2020 Anticrm Platform Contributors.
|
||||
//
|
||||
// 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 { afterUpdate, createEventDispatcher } from 'svelte'
|
||||
import { IconNavPrev, IconNavNext, Icon } from '../..'
|
||||
import { firstDay, day, getWeekDayName, areDatesEqual, getMonthName, weekday, isWeekend } from './internal/DateUtils'
|
||||
|
||||
export let currentDate: Date | null
|
||||
export let viewDate: Date
|
||||
export let mondayStart: boolean = true
|
||||
export let hideNavigator: boolean = false
|
||||
export let viewUpdate: boolean = true
|
||||
export let displayedWeeksCount = 6
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: firstDayOfCurrentMonth = firstDay(viewDate, mondayStart)
|
||||
let monthYear: string
|
||||
const today: Date = new Date(Date.now())
|
||||
const capitalizeFirstLetter = (str: string): string => str.charAt(0).toUpperCase() + str.slice(1)
|
||||
|
||||
if (viewDate == undefined) viewDate = currentDate ?? today
|
||||
afterUpdate(() => {
|
||||
if (currentDate && viewUpdate) viewDate = currentDate
|
||||
if (viewDate) {
|
||||
monthYear = capitalizeFirstLetter(getMonthName(viewDate)) + ' ' + viewDate.getFullYear()
|
||||
firstDayOfCurrentMonth = firstDay(viewDate, mondayStart)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="month-container">
|
||||
<div class="header">
|
||||
{#if viewDate}
|
||||
<div class="monthYear">{monthYear}</div>
|
||||
<div class="group" class:hideNavigator>
|
||||
<div class="btn" on:click={() => {
|
||||
if (viewUpdate) viewDate.setMonth(viewDate.getMonth() - 1)
|
||||
dispatch('navigation', '-m')
|
||||
}}>
|
||||
<div class="icon-btn"><Icon icon={IconNavPrev} size={'full'} /></div>
|
||||
</div>
|
||||
<div class="btn" on:click={() => {
|
||||
if (viewUpdate) viewDate.setMonth(viewDate.getMonth() + 1)
|
||||
dispatch('navigation', '+m')
|
||||
}}>
|
||||
<div class="icon-btn"><Icon icon={IconNavNext} size={'full'} /></div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if viewDate}
|
||||
<div class="calendar">
|
||||
{#each [...Array(7).keys()] as dayOfWeek}
|
||||
<span class="caption">{capitalizeFirstLetter(getWeekDayName(day(firstDayOfCurrentMonth, dayOfWeek), 'short'))}</span>
|
||||
{/each}
|
||||
|
||||
{#each [...Array(displayedWeeksCount).keys()] as weekIndex}
|
||||
{#each [...Array(7).keys()] as dayOfWeek}
|
||||
<div
|
||||
class="day"
|
||||
class:weekend={isWeekend(weekday(firstDayOfCurrentMonth, weekIndex, dayOfWeek))}
|
||||
class:today={areDatesEqual(today, weekday(firstDayOfCurrentMonth, weekIndex, dayOfWeek))}
|
||||
class:selected={currentDate && weekday(firstDayOfCurrentMonth, weekIndex, dayOfWeek).getMonth() ===
|
||||
currentDate.getMonth() && areDatesEqual(currentDate, weekday(firstDayOfCurrentMonth, weekIndex, dayOfWeek))}
|
||||
class:wrongMonth={weekday(firstDayOfCurrentMonth, weekIndex, dayOfWeek).getMonth() !==
|
||||
viewDate.getMonth()}
|
||||
style={`grid-column-start: ${dayOfWeek + 1}; grid-row-start: ${weekIndex + 2};`}
|
||||
on:click|stopPropagation={() => {
|
||||
viewDate = weekday(firstDayOfCurrentMonth, weekIndex, dayOfWeek)
|
||||
if (currentDate) {
|
||||
viewDate.setHours(currentDate.getHours())
|
||||
viewDate.setMinutes(currentDate.getMinutes())
|
||||
}
|
||||
dispatch('update', viewDate)
|
||||
}}
|
||||
>
|
||||
{weekday(firstDayOfCurrentMonth, weekIndex, dayOfWeek).getDate()}
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.month-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--theme-caption-color);
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 1rem .75rem;
|
||||
color: var(--caption-color);
|
||||
|
||||
.monthYear {
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
&::first-letter { text-transform: capitalize; }
|
||||
}
|
||||
.group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.hideNavigator { visibility: hidden; }
|
||||
.btn {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 1.25rem;
|
||||
height: 1.75rem;
|
||||
color: var(--dark-color);
|
||||
cursor: pointer;
|
||||
|
||||
.icon-btn { height: .75rem; }
|
||||
&:hover { color: var(--accent-color); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.calendar {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: .5rem;
|
||||
padding: 0 1rem 1rem;
|
||||
|
||||
.caption, .day {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 1.625rem;
|
||||
height: 1.625rem;
|
||||
font-size: 1rem;
|
||||
color: var(--content-color);
|
||||
}
|
||||
.caption {
|
||||
align-items: start;
|
||||
height: 2rem;
|
||||
color: var(--dark-color);
|
||||
&::first-letter { text-transform: capitalize; }
|
||||
}
|
||||
.day {
|
||||
position: relative;
|
||||
color: var(--accent-color);
|
||||
background-color: rgba(var(--accent-color), .05);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
|
||||
&.weekend { color: var(--content-color); }
|
||||
&.wrongMonth { color: var(--dark-color); }
|
||||
&.today {
|
||||
font-weight: 500;
|
||||
color: var(--caption-color);
|
||||
background-color: var(--button-bg-color);
|
||||
border-color: var(--dark-color);
|
||||
}
|
||||
&.selected, &:hover {
|
||||
color: var(--caption-color);
|
||||
background-color: var(--primary-bg-color);
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -.625rem;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
top: 2rem;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: var(--button-bg-color);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
export let size: 'x-small' | 'small' | 'medium' | 'large' | 'full'
|
||||
export let fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" {fill} viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15 5C15 2.79086 13.2091 1 11 1H5C2.79086 1 1 2.79086 1 5V11C1 13.2091 2.79086 15 5 15H6.25C6.66421 15 7 14.6642 7 14.25C7 13.8358 6.66421 13.5 6.25 13.5H5C3.61929 13.5 2.5 12.3807 2.5 11V6H13.5V6.25C13.5 6.66421 13.8358 7 14.25 7C14.6642 7 15 6.66421 15 6.25V5ZM11.5001 8C11.9143 8 12.2501 8.33579 12.2501 8.75V10.75L14.2501 10.75C14.6643 10.75 15.0001 11.0858 15.0001 11.5C15.0001 11.9142 14.6643 12.25 14.2501 12.25L12.2501 12.25V14.25C12.2501 14.6642 11.9143 15 11.5001 15C11.0859 15 10.7501 14.6642 10.7501 14.25V12.25H8.75C8.33579 12.25 8 11.9142 8 11.5C8 11.0858 8.33579 10.75 8.75 10.75L10.7501 10.75V8.75C10.7501 8.33579 11.0859 8 11.5001 8Z" />
|
||||
</svg>
|
@ -44,6 +44,11 @@ export default plugin(uiId, {
|
||||
StartDate: '' as IntlString,
|
||||
TargetDate: '' as IntlString,
|
||||
Overdue: '' as IntlString,
|
||||
DueDate: '' as IntlString,
|
||||
AddDueDate: '' as IntlString,
|
||||
EditDueDate: '' as IntlString,
|
||||
SaveDueDate: '' as IntlString,
|
||||
IssueNeedsToBeCompletedByThisDate: '' as IntlString,
|
||||
English: '' as IntlString,
|
||||
Russian: '' as IntlString,
|
||||
MinutesBefore: '' as IntlString,
|
||||
|
@ -63,7 +63,7 @@ export interface Tab {
|
||||
|
||||
export type TabModel = Tab[]
|
||||
|
||||
export type PopupAlignment = HTMLElement | EventTarget | null | 'right' | 'float' | 'account' | 'full' | 'content' | 'middle'
|
||||
export type PopupAlignment = HTMLElement | EventTarget | null | 'right' | 'top' | 'account' | 'full' | 'content' | 'middle'
|
||||
|
||||
export type TooltipAligment = 'top' | 'bottom' | 'left' | 'right'
|
||||
|
||||
|
@ -77,7 +77,4 @@
|
||||
justify-content: center;
|
||||
background-color: var(--theme-dialog-accent);
|
||||
}
|
||||
.title {
|
||||
align-self: flex-start;
|
||||
}
|
||||
</style>
|
||||
|
@ -19,7 +19,7 @@
|
||||
import { getClient, UserBox } from '@anticrm/presentation'
|
||||
import { Issue, IssuePriority, IssueStatus, Team } from '@anticrm/tracker'
|
||||
import { StyledTextBox } from '@anticrm/text-editor'
|
||||
import { EditBox, Button, showPopup } from '@anticrm/ui'
|
||||
import { EditBox, Button, showPopup, DatePresenter } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../plugin'
|
||||
import { calcRank } from '../utils'
|
||||
@ -92,6 +92,7 @@
|
||||
{ icon: tracker.icon.DueDate, label: tracker.string.DueDate },
|
||||
{ icon: tracker.icon.Parent, label: tracker.string.Parent }
|
||||
]
|
||||
let issueDate: number | null = null
|
||||
</script>
|
||||
|
||||
<!-- canSave: object.title.length > 0 && _space != null -->
|
||||
@ -150,6 +151,7 @@
|
||||
size="small"
|
||||
kind="no-border"
|
||||
/>
|
||||
<DatePresenter value={issueDate} editable />
|
||||
<Button
|
||||
icon={tracker.icon.MoreActions}
|
||||
width="min-content"
|
||||
@ -159,7 +161,5 @@
|
||||
showPopup(SelectPopup, { value: moreActions }, ev.currentTarget)
|
||||
}}
|
||||
/>
|
||||
<!-- <DateRangePresenter value={startDate} labelNull={ui.string.StartDate} editable />
|
||||
<DateRangePresenter value={targetDate} labelNull={ui.string.TargetDate} editable /> -->
|
||||
</div>
|
||||
</Card>
|
||||
|
@ -6,7 +6,7 @@
|
||||
export let currentSpace: Ref<Space>
|
||||
|
||||
async function newIssue(target: EventTarget | null): Promise<void> {
|
||||
showPopup(CreateIssue, { space: currentSpace }, target as HTMLElement)
|
||||
showPopup(CreateIssue, { space: currentSpace }, 'top')
|
||||
}
|
||||
</script>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user