Update DatePicker (#1337)

Signed-off-by: Alexander Platov <sas_lord@mail.ru>
This commit is contained in:
Alexander Platov 2022-04-08 21:17:04 +03:00 committed by GitHub
parent b695538e12
commit 28329f74bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 947 additions and 845 deletions

View File

@ -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 {

View File

@ -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);

View File

@ -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}}",

View File

@ -19,6 +19,11 @@
"StartDate": "Дата начала",
"TargetDate": "Дата окончания",
"Overdue": "Просрочено",
"DueDate": "Срок",
"AddDueDate": "Установить дату",
"EditDueDate": "Изменить дату",
"SaveDueDate": "Сохранить дату",
"IssueNeedsToBeCompletedByThisDate": "Задача должна быть завершена к этой дате",
"English": "Английский",
"Russian": "Русский",
"MinutesBefore": "{minutes, plural, =1 {за минуту} other {за # минут}}",

View File

@ -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 {

View File

@ -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 {

View File

@ -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'

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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>

View 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>

View 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>

View File

@ -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>

View File

@ -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,

View File

@ -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'

View File

@ -77,7 +77,4 @@
justify-content: center;
background-color: var(--theme-dialog-accent);
}
.title {
align-self: flex-start;
}
</style>

View File

@ -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>

View File

@ -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>