Update DataPicker layout (#1309)

Signed-off-by: Alexander Platov <sas_lord@mail.ru>
This commit is contained in:
Alexander Platov 2022-04-07 13:02:01 +03:00 committed by GitHub
parent 424107312d
commit 30976696f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1473 additions and 324 deletions

View File

@ -61,11 +61,14 @@
--accent-bg-color: #27282b;
--accent-shadow: rgb(0 0 0 / 10%) 0px 2px 4px;
--dark-color: #62666d;
--content-color: #8a8f98;
--accent-color: #d7d8db;
--caption-color: #f7f8f8;
--white-color: #fff;
--caret-color: #6e5ed2;
--warning-color: #f2994a;
--error-color: #eb5757;
--divider-color: #303236;
--menu-bg-select: #2d2f36;

View File

@ -15,6 +15,10 @@
"None": "None",
"NotSelected": "Not selected",
"Today": "Today",
"NoDate": "No date",
"StartDate": "Start date",
"TargetDate": "Target date",
"Overdue": "Overdue",
"English": "English",
"Russian": "Russian",
"MinutesBefore": "{minutes, plural, =1 {a minute before} other {# minutes before}}",

View File

@ -15,6 +15,10 @@
"None": "Нет",
"NotSelected": "Не выбрано",
"Today": "Сегодня",
"NoDate": "Без даты",
"StartDate": "Дата начала",
"TargetDate": "Дата окончания",
"Overdue": "Просрочено",
"English": "Английский",
"Russian": "Русский",
"MinutesBefore": "{minutes, plural, =1 {за минуту} other {за # минут}}",

View File

@ -107,7 +107,7 @@
pointer-events: none;
}
&:hover {
color: var(--caption-color);
color: var(--accent-color);
transition-duration: 0;
.btn-icon { color: var(--caption-color); }
@ -137,15 +137,15 @@
}
&.no-border {
font-weight: 400;
color: var(--content-color);
color: var(--accent-color);
background-color: var(--button-bg-color);
box-shadow: var(--button-shadow);
&:hover {
color: var(--accent-color);
color: var(--caption-color);
background-color: var(--button-bg-hover);
.btn-icon { color: var(--accent-color); }
.btn-icon { color: var(--caption-color); }
}
&:disabled {
background-color: #30323655;

View File

@ -15,7 +15,9 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import type { IntlString } from '@anticrm/platform'
import { Label, DatePresenter } from '../..'
import Label from '../Label.svelte'
import DatePresenter from './DatePresenter.svelte'
import DatePopup from './DatePopup.svelte'
export let title: IntlString
export let value: number | null | undefined = null

View File

@ -0,0 +1,129 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { afterUpdate } from 'svelte'
import type { AnySvelteComponent } from '../../types'
import { dpstore } from '../../popups'
let component: AnySvelteComponent | undefined
let anchor: HTMLElement
let modalHTML: HTMLElement
let frendlyFocus: HTMLElement[] | undefined
let componentInstance: any
$: component = $dpstore.component
$: frendlyFocus = $dpstore.frendlyFocus
$: if ($dpstore.anchor) {
anchor = $dpstore.anchor
if (modalHTML) $dpstore.popup = modalHTML
}
$: component = $dpstore.component
function _update (result: any): void {
fitPopup()
}
function _change (result: any): void {
if ($dpstore.onChange !== undefined) $dpstore.onChange(result)
}
function _close (result: any): void {
if ($dpstore.onClose !== undefined) $dpstore.onClose(result)
}
function escapeClose () {
if (componentInstance && componentInstance.canClose) {
if (!componentInstance.canClose()) return
}
_close(null)
}
const fitPopup = (): void => {
if (modalHTML && component) {
if (componentInstance) {
modalHTML.style.left = modalHTML.style.right = modalHTML.style.top = modalHTML.style.bottom = ''
modalHTML.style.maxHeight = modalHTML.style.height = ''
const rect = anchor.getBoundingClientRect()
const rectPopup = modalHTML.getBoundingClientRect()
// Vertical
if (rect.bottom + rectPopup.height + 28 <= document.body.clientHeight) {
modalHTML.style.top = `calc(${rect.bottom}px + .5rem)`
} else if (rectPopup.height + 28 < rect.top) {
modalHTML.style.bottom = `calc(${document.body.clientHeight - rect.y}px + .5rem)`
} else modalHTML.style.top = `calc(${rect.bottom}px + .5rem)`
// Horizontal
if (rect.left + rectPopup.width + 16 > document.body.clientWidth) {
modalHTML.style.right = document.body.clientWidth - rect.right + 'px'
} else {
modalHTML.style.left = rect.left + 'px'
}
}
}
}
function handleKeydown (ev: KeyboardEvent) {
if (ev.key === 'Escape' && component) {
escapeClose()
}
}
afterUpdate(() => fitPopup())
const unfocus = (ev: FocusEvent): void => {
const target = ev.relatedTarget as HTMLElement
let kl: boolean = false
frendlyFocus?.forEach(edit => { if (edit === target) kl = true })
if (target === modalHTML) kl = true
if (!kl || target === null) _close(null)
}
</script>
<svelte:window on:resize={fitPopup} on:keydown={handleKeydown} />
<div
class="popup"
class:visibility={$dpstore.component !== undefined}
bind:this={modalHTML}
tabindex="0"
on:blur={(ev) => unfocus(ev)}
>
{#if $dpstore.component}
<svelte:component
bind:this={componentInstance}
this={component}
on:update={(ev) => _update(ev.detail)}
on:change={(ev) => _change(ev.detail)}
on:close={(ev) => _close(ev.detail)}
/>
{/if}
</div>
<style lang="scss">
.popup {
visibility: hidden;
position: fixed;
display: flex;
flex-direction: column;
justify-content: center;
max-height: calc(100vh - 2rem);
background-color: transparent;
outline: none;
z-index: 11000;
&.visibility { visibility: visible; }
}
</style>

View File

@ -51,9 +51,7 @@
const tempDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), i)
days.push({
dayOfWeek: (tempDate.getDay() === 0) ? 7 : tempDate.getDay(),
style: getDateStyle(tempDate),
focused: false,
today: areDatesEqual(tempDate, today)
style: getDateStyle(tempDate)
})
}
days = days
@ -92,9 +90,6 @@
{#each days as day, i}
<div
class="day {day.style}"
class:today={day.today}
class:focused={day.focused}
data-today={day.today ? todayString : ''}
style="grid-column: {day.dayOfWeek}/{day.dayOfWeek + 1};"
on:click|stopPropagation={() => {
if (currentDate) currentDate.setDate(i + 1)

View File

@ -13,54 +13,63 @@
// limitations under the License.
-->
<script lang="ts">
import { translate } from '@anticrm/platform'
import { onMount, createEventDispatcher, afterUpdate, onDestroy } from 'svelte'
import { daysInMonth } from './internal/DateUtils'
import { TimePopup, tooltipstore as tooltip, showTooltip } from '../..'
import DatePopup from './DatePopup.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 DPCalendar from './icons/DPCalendar.svelte'
export let value: number | null | undefined
export let withDate: boolean = true
export let withTime: boolean = false
export let mondayStart: boolean = true
export let editable: boolean = false
const INPUT_WIDTH_INCREMENT = 2
const dispatch = createEventDispatcher()
type TEdits = 'day' | 'month' | 'year' | 'hour' | 'min'
interface IEdits {
id: TEdits
numeric: number
value: string
el?: HTMLInputElement
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 dateDiv: HTMLElement
let timeDiv: HTMLElement
let dateBox: HTMLElement
let timeBox: HTMLElement
let dateContainer: HTMLElement
let text: HTMLElement
let todayString: string
translate(ui.string.Today, {}).then(res => todayString = res)
let dateShow: boolean = false
$: dateShow = !!($tooltip.label || $tooltip.component)
let edit: boolean = false
let startTyping: boolean = false
let datePresenter: HTMLElement
let datePopup: HTMLElement
let closeBtn: HTMLElement
function computeSize (t: HTMLInputElement | EventTarget | null, ed: TEdits) {
const target = t as HTMLInputElement
const val = (target.value === '' || target.value.length < 2) ? '00' : target.value
text.innerHTML = val.replaceAll(' ', '&nbsp;')
target.style.width = text.clientWidth + INPUT_WIDTH_INCREMENT + 'px'
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 edits: IEdits[] = [{ id: 'day', numeric: 0, value: '0' }, { id: 'month', numeric: 0, value: '0' },
{ id: 'year', numeric: 0, value: '0' }, { id: 'hour', numeric: 0, value: '0' },
{ id: 'min', numeric: 0, value: '0' }]
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) {
@ -103,313 +112,468 @@
const dateToEdits = (): void => {
edits.forEach(edit => {
edit.numeric = getValue(currentDate, edit.id)
if (value !== undefined && value !== null) edit.value = edit.numeric.toString().padStart(edit.id === 'year' ? 4 : 2, '0')
else edit.value = (edit.id === 'year') ? '----' : '--'
if (edit.el) {
edit.el.value = edit.value
computeSize(edit.el, edit.id)
}
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()
$tooltip.props = { value }
renderCellStyles()
dispatch('change', value)
}
$: if (value) dateToEdits()
const onInput = (t: HTMLInputElement | EventTarget | null, ed: TEdits) => {
const target = t as HTMLInputElement
const val: number = Number(target.value)
if (isNaN(val)) {
target.classList.add('wrong-input')
edits[getIndex(ed)].el?.select()
} else {
target.classList.remove('wrong-input')
if (val > getMaxValue(currentDate ?? today, ed)) {
setValue(getMaxValue(currentDate ?? today, ed), currentDate ?? today, ed)
target.classList.add('wrong-input')
edits[getIndex(ed)].el?.select()
} else {
currentDate = setValue(val, currentDate ?? today, ed)
saveDate()
if (ed === 'day' && val > daysInMonth(currentDate ?? today) / 10) edits[1].el?.select()
else if (ed === 'month' && val > 1) edits[2].el?.select()
else if (ed === 'year' && val > 1900 && withTime) edits[3].el?.select()
else if (ed === 'hour' && val > 2) edits[4].el?.select()
}
dateToEdits()
}
computeSize(t, ed)
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 {
value = null
dispatch('change', null)
}
edit = false
}
const keyDown = (ev: KeyboardEvent, ed: TEdits): void => {
const target = ev.target as HTMLInputElement
const target = ev.target as HTMLElement
const index = getIndex(ed)
if (ev.code === 'Backspace') {
target.value = ''
target.classList.remove('wrong-input')
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 === 'ArrowUp' || ev.code === 'ArrowDown' && target.value !== '') {
let val = (ev.code === 'ArrowUp')
? edits[index].numeric + 1
: edits[index].numeric - 1
if (currentDate) {
target.classList.remove('wrong-input')
currentDate = setValue(val, currentDate, ed)
saveDate()
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()
}
}
}
}
const updateFromTooltip = (result: any): void => {
if (result.detail !== undefined) {
currentDate = new Date(result.detail)
saveDate()
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 hoverEdits = (ev: MouseEvent, t: 'date' | 'time'): void => {
if (!dateShow) showTooltip(undefined,
t === 'date' ? dateBox : timeBox,
undefined, t === 'date' ? DatePopup : TimePopup,
{ value: currentDate ?? today },
undefined,
updateFromTooltip)
// $tooltip.props = { value }
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()
}
const editClick = (ev: MouseEvent, el: TEdits): void => {
ev.stopPropagation()
const target = ev.target as HTMLInputElement
target.select()
}
$: if (selected && edits[getIndex(selected)].el) edits[getIndex(selected)].el?.focus()
afterUpdate(() => {
const tempEl = edits[getIndex(selected)].el
if (tempEl) tempEl.focus()
})
onMount(() => { dateToEdits() })
// onDestroy(() => { closeTooltip() })
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'
}
</script>
{#if currentDate !== undefined}
<div bind:this={dateContainer} class="datetime-presenter-container">
{#if editable}
<div bind:this={dateDiv} class="flex-row-center flex-no-shrink flex-nowrap">
<div class="datetime-icon" class:selected={withDate} on:click|stopPropagation={() => { withDate = !withDate }}>
<div class="icon"><DPCalendar size={'full'} /></div>
</div>
<div class="hidden-text" bind:this={text} />
<div
bind:this={dateBox}
class="datetime-presenter antiWrapper conners focusWI dateBox"
class:zero={!withDate}
on:mousemove={(ev) => hoverEdits(ev, 'date')}
>
<input
type="text" placeholder={'00'} class="zone" class:selected={selected === edits[0].id}
bind:this={edits[0].el} bind:value={edits[0].value}
on:click|stopPropagation={ev => editClick(ev, edits[0].id)}
on:input={ev => onInput(ev.target, edits[0].id)}
on:keydown={ev => keyDown(ev, edits[0].id)}
/>
<div class="symbol">.</div>
<input
type="text" placeholder={'00'} class="zone" class:selected={selected === edits[1].id}
bind:this={edits[1].el} bind:value={edits[1].value}
on:click|stopPropagation={ev => editClick(ev, edits[1].id)}
on:input={ev => onInput(ev.target, edits[1].id)}
on:keydown={ev => keyDown(ev, edits[1].id)}
/>
<div class="symbol">.</div>
<input
type="text" placeholder={'0000'} class="zone" class:selected={selected === edits[2].id}
bind:this={edits[2].el} bind:value={edits[2].value}
on:click|stopPropagation={ev => editClick(ev, edits[2].id)}
on:input={ev => onInput(ev.target, edits[2].id)}
on:keydown={ev => keyDown(ev, edits[2].id)}
/>
</div>
</div>
<div class="datetime-icon" class:selected={withTime} on:click|stopPropagation={() => { withTime = !withTime }}>
<div class="icon"><DPClock size={'full'} /></div>
</div>
<div
bind:this={timeBox}
class="datetime-presenter antiWrapper conners focusWI timeBox"
class:zero={!withTime}
on:mousemove={(ev) => hoverEdits(ev, 'time')}
<button
bind:this={datePresenter}
class="button normal"
class:edit
class:editable
on:click={() => { if (editable) edit = true }}
>
{#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)}
>
<input
type="text" placeholder={'00'} class="zone" class:selected={selected === edits[3].id}
bind:this={edits[3].el} bind:value={edits[3].value}
on:click|stopPropagation={ev => editClick(ev, edits[3].id)}
on:input={ev => onInput(ev.target, edits[3].id)}
on:keydown={ev => keyDown(ev, edits[3].id)}
/>
<div class="symbol">:</div>
<input
type="text" placeholder={'00'} class="zone" class:selected={selected === edits[4].id}
bind:this={edits[4].el} bind:value={edits[4].value}
on:click|stopPropagation={ev => editClick(ev, edits[4].id)}
on:input={ev => onInput(ev.target, edits[4].id)}
on:blur={ev => onInput(ev.target, edits[4].id)}
on:keydown={ev => keyDown(ev, edits[4].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>
{#if new Date(value).getFullYear() !== today.getFullYear()}
<span class="digit">{new Date(value).getFullYear()}</span>
{/if}
{#if withTime}
<div class="time-divider" />
<span class="digit">{new Date(value).getHours().toString().padStart(2, '0')}</span>
<span class="separator">:</span>
<span class="digit">{new Date(value).getMinutes().toString().padStart(2, '0')}</span>
{/if}
{:else}
<div class="flex-col">
<div bind:this={dateDiv} class="datetime-presenter readable">
<div class="preview-icon"><DPCalendar size={'full'} /></div>
{currentDate.getDate().toString().padStart(2, '0')}
<div class="symbol">.</div>
{currentDate.getMonth().toString().padStart(2, '0')}
<div class="symbol">.</div>
{currentDate.getFullYear()}
</div>
{#if withTime}
<div bind:this={timeDiv} class="datetime-presenter readable">
<div class="preview-icon"><DPClock size={'full'} /></div>
{currentDate.getHours().toString().padStart(2, '0')}
<div class="symbol">:</div>
{currentDate.getMinutes().toString().padStart(2, '0')}
No date
{/if}
{/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>
{/if}
{/each}
</div>
{/if}
</div>
{/if}
</div>
<style lang="scss">
.datetime-presenter-container {
.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 {
position: relative;
display: flex;
align-items: center;
flex-wrap: nowrap;
flex-shrink: 0;
min-width: 0;
padding: 0 .5rem;
font-weight: 500;
min-width: 1.5rem;
width: auto;
border-radius: .75rem;
}
.datetime-presenter {
flex-shrink: 0;
border-radius: .5rem;
&.readable {
display: flex;
align-items: center;
flex-wrap: nowrap;
flex-shrink: 0;
min-width: 0;
color: var(--theme-content-accent-color);
.symbol { margin: 0 .125rem; }
}
.zone {
position: relative;
display: flex;
align-items: center;
font-weight: 500;
color: var(--theme-content-accent-color);
cursor: pointer;
z-index: 1;
&::before, &::after {
position: absolute;
top: 0;
left: -.125rem;
width: calc(100% + .25rem);
height: 100%;
border-radius: .75rem;
z-index: -1;
}
&::before { background-color: var(--primary-button-outline); }
&::after { background-color: var(--theme-bg-accent-hover); }
&:hover::after { content: ''; }
}
.symbol {
font-weight: 500;
color: var(--theme-content-dark-color);
}
}
.datetime-icon {
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
width: 1.25rem;
height: 1.25rem;
color: var(--theme-content-color);
background-color: var(--theme-bg-accent-color);
border-radius: .75rem;
&:hover {
color: var(--theme-caption-color);
background-color: var(--theme-bg-accent-hover);
}
&.selected {
margin-right: .25rem;
color: var(--theme-content-accent-color);
background-color: var(--theme-bg-accent-hover);
&:hover {
color: var(--theme-caption-color);
background-color: var(--theme-bg-focused-color);
}
}
.icon {
overflow: hidden;
width: .75rem;
height: .75rem;
}
}
.preview-icon {
margin-right: .25rem;
width: .75rem;
height: .75rem;
color: var(--theme-content-dark-color);
}
input {
margin: 0;
padding: 0;
width: 1.5rem;
height: 1.25rem;
color: var(--theme-caption-color);
border: transparent;
height: 1.5rem;
white-space: nowrap;
line-height: 1.5rem;
color: var(--accent-color);
background-color: transparent;
border: 1px solid transparent;
border-radius: .25rem;
text-align: center;
transition: background-color .2s ease-out;
transition-property: border, background-color, color, box-shadow;
transition-duration: .15s;
cursor: default;
&:hover { background-color: var(--theme-bg-accent-hover); }
&:focus {
color: var(--theme-bg-color);
background-color: var(--primary-button-enabled);
.btn-icon {
color: var(--content-color);
transition: color .15s;
pointer-events: none;
}
&::placeholder { color: var(--theme-content-dark-color); }
}
&:hover { transition-duration: 0; }
&:focus-within .datepopup-container { visibility: visible; }
.timeBox, .dateBox {
visibility: visible;
display: flex;
align-items: center;
min-width: 0;
max-width: 100%;
width: auto;
opacity: 1;
&.normal {
font-weight: 400;
color: var(--content-color);
background-color: var(--button-bg-color);
box-shadow: var(--button-shadow);
&.zero {
&.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;
}
}
}
&:disabled {
background-color: #30323655;
cursor: default;
&:hover {
color: var(--content-color);
.btn-icon { color: var(--content-color); }
}
}
.close-btn {
margin: 0 .25rem;
width: .75rem;
height: .75rem;
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);
}
}
}
&.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;
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;
width: 0;
max-width: 0;
opacity: 0;
position: absolute;
top: calc(100% + .5rem);
left: 50%;
width: auto;
height: auto;
transform: translateX(-50%);
z-index: 10000;
}
}
.dateBox { margin-right: .25rem; }
</style>

View File

@ -0,0 +1,44 @@
<!--
// 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 { createEventDispatcher } from 'svelte'
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
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()
const changeValue = (result: any): void => {
if (result.detail !== undefined) {
value = result.detail
dispatch('change', value)
}
}
</script>
<div class="antiSelect antiWrapper cursor-default">
<div class="flex-col">
<span class="label mb-1"><Label label={title} /></span>
<DateRangePresenter {value} {withTime} {icon} {labelOver} {labelNull} editable on:change={changeValue} />
</div>
</div>

View File

@ -0,0 +1,210 @@
<!--
// 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 { 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'
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
}
</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>
<style lang="scss">
.daterange-popup-container {
display: flex;
flex-direction: column;
min-height: 0;
width: 100%;
height: 100%;
color: var(--theme-caption-color);
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

@ -0,0 +1,474 @@
<!--
// 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 type { IntlString } from '@anticrm/platform'
import { createEventDispatcher, afterUpdate } from 'svelte'
import { daysInMonth, getMonthName } from './internal/DateUtils'
import ui, { Label, Icon, IconClose } from '../..'
import { dpstore, closeDatePopup } from '../../popups'
import DPCalendar from './icons/DPCalendar.svelte'
import DPCalendarOver from './icons/DPCalendarOver.svelte'
import DateRangePopup from './DateRangePopup.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())
let currentDate: Date = new Date(value ?? Date.now())
currentDate.setSeconds(0, 0)
let selected: TEdits = 'day'
let edit: boolean = false
let opened: boolean = false
let startTyping: boolean = false
let datePresenter: HTMLElement
let closeBtn: HTMLElement
let edits: IEdits[] = editsType.map(edit => { return { id: edit, value: -1 } })
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()
$dpstore.currentDate = currentDate
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 closeDP = (): void => {
if (!isNull()) saveDate()
else {
value = null
dispatch('change', null)
}
closeDatePopup()
edit = opened = 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)
$dpstore.currentDate = currentDate
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') 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)
$dpstore.currentDate = currentDate
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)) closeDP()
}
}
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 === popupComp || target === closeBtn) kl = true
if (!kl || target === null) closeDP()
}
$: if (selected && edits[getIndex(selected)].el) edits[getIndex(selected)].el?.focus()
afterUpdate(() => {
const tempEl = edits[getIndex(selected)].el
if (tempEl) tempEl.focus()
})
const _change = (result: any): void => {
if (result !== undefined) {
currentDate = result
saveDate()
}
}
const _close = (result: any): void => {
if (result !== undefined) {
if (result !== null) {
currentDate = result
saveDate()
}
closeDP()
}
}
const openPopup = (): void => {
opened = edit = true
$dpstore.currentDate = currentDate
$dpstore.anchor = datePresenter
$dpstore.onChange = _change
$dpstore.onClose = _close
$dpstore.component = DateRangePopup
}
let popupComp: HTMLElement
$: if (opened && $dpstore.popup) popupComp = $dpstore.popup
$: if (opened && edits[0].el && $dpstore.frendlyFocus === undefined) {
let frendlyFocus: HTMLElement[] = []
edits.forEach((edit, i) => {
if (edit.el) frendlyFocus[i] = edit.el
})
frendlyFocus.push(closeBtn)
$dpstore.frendlyFocus = frendlyFocus
}
</script>
<button
bind:this={datePresenter}
class="datetime-button"
class:editable
class:edit
on:click={() => { if (editable && !opened) openPopup() }}
>
{#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 (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={(ev) => unfocus(ev, edits[1].id)}
>
{#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={(ev) => unfocus(ev, edits[2].id)}
>
{#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={(ev) => unfocus(ev, edits[3].id)}
>
{#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={(ev) => unfocus(ev, edits[4].id)}
>
{#if (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 {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()}
{new Date(value).getFullYear()}
{/if}
{#if withTime}
<div class="time-divider" />
{new Date(value).getHours().toString().padStart(2, '0')}
<span class="separator">:</span>
{new Date(value).getMinutes().toString().padStart(2, '0')}
{/if}
{/if}
{:else}
<Label label={labelNull} />
{/if}
{/if}
</button>
<style lang="scss">
.datetime-button {
position: relative;
display: flex;
align-items: center;
flex-shrink: 0;
padding: 0 .5rem;
font-weight: 400;
font-size: .8125rem;
min-width: 1.5rem;
width: auto;
height: 1.5rem;
white-space: nowrap;
line-height: 1.5rem;
color: var(--accent-color);
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 {
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 {
color: var(--caption-color);
transition-duration: 0;
}
&.editable {
cursor: pointer;
&: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); }
}
&:focus-within {
border-color: var(--primary-edit-border-color);
&:hover { background-color: transparent; }
}
}
&:disabled {
background-color: #30323655;
cursor: default;
&:hover {
color: var(--content-color);
.btn-icon { color: var(--content-color); }
}
}
&.edit {
padding: 0 .125rem;
background-color: transparent;
border-color: var(--primary-edit-border-color);
&:hover { background-color: transparent; }
}
.close-btn {
display: flex;
justify-content: center;
align-items: center;
margin: 0 .25rem;
width: .75rem;
height: .75rem;
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);
}
}
.digit {
position: relative;
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); }
&::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);
}
}
</style>

View File

@ -16,6 +16,8 @@
import DatePresenter from './DatePresenter.svelte'
export let value: number | null | undefined
export let mondayStart: boolean = true
export let editable: boolean = false
</script>
<DatePresenter {value} withTime />
<DatePresenter bind:value withTime {mondayStart} {editable} />

View File

@ -0,0 +1,28 @@
<!--
// 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 type { IntlString } from '@anticrm/platform'
import ui from '../..'
import DateRangePresenter from './DateRangePresenter.svelte'
export let value: number | null | undefined
export let mondayStart: boolean = true
export let editable: boolean = false
export let icon: 'normal' | 'warning' | 'overdue' = 'normal'
export let labelOver: IntlString | undefined = undefined
export let labelNull: IntlString = ui.string.NoDate
</script>
<DateRangePresenter bind:value withTime {mondayStart} {editable} {icon} {labelOver} {labelNull} />

View File

@ -3,6 +3,6 @@
export let fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M16.8,2.8V2c0-0.4-0.3-0.8-0.8-0.8S15.2,1.6,15.2,2v0.8H8.8V2c0-0.4-0.3-0.8-0.8-0.8S7.2,1.6,7.2,2v0.8 c-3.2,0.3-5,2.3-5,5.7V17c0,3.7,2.1,5.8,5.8,5.8h8c3.7,0,5.8-2.1,5.8-5.8V8.5C21.8,5.1,19.9,3.1,16.8,2.8z M7.2,4.3V5 c0,0.4,0.3,0.8,0.8,0.8S8.8,5.4,8.8,5V4.2h6.5V5c0,0.4,0.3,0.8,0.8,0.8s0.8-0.3,0.8-0.8V4.3c2.3,0.2,3.4,1.6,3.5,4H3.8 C3.8,5.9,5,4.5,7.2,4.3z M16,21.2H8c-2.9,0-4.2-1.4-4.2-4.2V9.8h16.5V17C20.2,19.9,18.9,21.2,16,21.2z"/>
<svg class="svg-{size}" {fill} viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="M11 1C13.2091 1 15 2.79086 15 5V11C15 13.2091 13.2091 15 11 15H5C2.79086 15 1 13.2091 1 11V5C1 2.79086 2.79086 1 5 1H11ZM13.5 6H2.5V11C2.5 12.3807 3.61929 13.5 5 13.5H11C12.3807 13.5 13.5 12.3807 13.5 11V6Z"/>
</svg>

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="M11 1C13.2091 1 15 2.79086 15 5V6.25C15 6.66421 14.6642 7 14.25 7C13.8358 7 13.5 6.66421 13.5 6.25V6H2.5V11C2.5 12.3807 3.61929 13.5 5 13.5H6.25C6.66421 13.5 7 13.8358 7 14.25C7 14.6642 6.66421 15 6.25 15H5C2.79086 15 1 13.2091 1 11V5C1 2.79086 2.79086 1 5 1H11ZM9.53033 8.46967L11.5 10.4393L13.4697 8.46967C13.7626 8.17678 14.2374 8.17678 14.5303 8.46967C14.8232 8.76256 14.8232 9.23744 14.5303 9.53033L12.5607 11.5L14.5303 13.4697C14.8232 13.7626 14.8232 14.2374 14.5303 14.5303C14.2374 14.8232 13.7626 14.8232 13.4697 14.5303L11.5 12.5607L9.53033 14.5303C9.23744 14.8232 8.76256 14.8232 8.46967 14.5303C8.17678 14.2374 8.17678 13.7626 8.46967 13.4697L10.4393 11.5L8.46967 9.53033C8.17678 9.23744 8.17678 8.76256 8.46967 8.46967C8.76256 8.17678 9.23744 8.17678 9.53033 8.46967Z" />
</svg>

View File

@ -72,17 +72,15 @@ export function isWeekend (date: Date): boolean {
return date.getDay() === 0 || date.getDay() === 6
}
export function getMonthName (date: Date): string {
export function getMonthName (date: Date, option: 'narrow' | 'short' | 'long' | 'numeric' | '2-digit' = 'long'): string {
const locale = new Intl.NumberFormat().resolvedOptions().locale
return new Intl.DateTimeFormat(locale, { month: 'long' }).format(date)
return new Intl.DateTimeFormat(locale, { month: option }).format(date)
}
export type TCellStyle = 'not-selected' | 'selected'
export interface ICell {
dayOfWeek: number
style: TCellStyle
focused: boolean
today?: boolean
}
export function getMonday (d: Date, mondayStart: boolean): Date {

View File

@ -0,0 +1,8 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 7 12">
<path d="M1,11.8c-0.2,0-0.4-0.1-0.5-0.2c-0.3-0.3-0.3-0.8,0-1.1l4.3-4.3c0.1-0.1,0.1-0.3,0-0.4L0.5,1.5 c-0.3-0.3-0.3-0.8,0-1.1s0.8-0.3,1.1,0l4.3,4.3C6.2,5.1,6.3,5.5,6.3,6S6.2,6.9,5.8,7.2l-4.3,4.3C1.4,11.7,1.2,11.8,1,11.8z" />
</svg>

View File

@ -0,0 +1,8 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 7 12">
<path d="M6,11.8c-0.2,0-0.4-0.1-0.5-0.2L1.2,7.2C0.8,6.9,0.7,6.5,0.7,6s0.2-0.9,0.5-1.2l4.3-4.3c0.3-0.3,0.8-0.3,1.1,0 s0.3,0.8,0,1.1L2.2,5.8c-0.1,0.1-0.1,0.3,0,0.4l4.3,4.3c0.3,0.3,0.3,0.8,0,1.1C6.4,11.7,6.2,11.8,6,11.8z" />
</svg>

View File

@ -45,9 +45,13 @@ export { default as PopupMenu } from './components/PopupMenu.svelte'
// export { default as PopupItem } from './components/PopupItem.svelte'
export { default as TextArea } from './components/TextArea.svelte'
export { default as Section } from './components/Section.svelte'
export { default as DatePickerPopup } from './components/calendar/DatePickerPopup.svelte'
export { default as DatePicker } from './components/calendar/DatePicker.svelte'
export { default as DateRangePicker } from './components/calendar/DateRangePicker.svelte'
export { default as DatePopup } from './components/calendar/DatePopup.svelte'
export { default as TimePopup } from './components/calendar/TimePopup.svelte'
export { default as DateRangePresenter } from './components/calendar/DateRangePresenter.svelte'
export { default as DateTimeRangePresenter } from './components/calendar/DateTimeRangePresenter.svelte'
export { default as DatePresenter } from './components/calendar/DatePresenter.svelte'
export { default as DateTimePresenter } from './components/calendar/DateTimePresenter.svelte'
export { default as StylishEdit } from './components/StylishEdit.svelte'
@ -95,6 +99,8 @@ export { default as IconInfo } from './components/icons/Info.svelte'
export { default as IconBlueCheck } from './components/icons/BlueCheck.svelte'
export { default as IconCheck } from './components/icons/Check.svelte'
export { default as IconArrowLeft } from './components/icons/ArrowLeft.svelte'
export { default as IconNavPrev } from './components/icons/NavPrev.svelte'
export { default as IconNavNext } from './components/icons/NavNext.svelte'
export { default as PanelInstance } from './components/PanelInstance.svelte'
export { default as Panel } from './components/Panel.svelte'

View File

@ -40,6 +40,10 @@ export default plugin(uiId, {
None: '' as IntlString,
NotSelected: '' as IntlString,
Today: '' as IntlString,
NoDate: '' as IntlString,
StartDate: '' as IntlString,
TargetDate: '' as IntlString,
Overdue: '' as IntlString,
English: '' as IntlString,
Russian: '' as IntlString,
MinutesBefore: '' as IntlString,

View File

@ -51,3 +51,55 @@ export function closePopup (): void {
return popups
})
}
interface IDatePopup {
component: AnySvelteComponent | undefined
currentDate: Date | undefined
anchor: HTMLElement | undefined
popup: HTMLElement | undefined
frendlyFocus: HTMLElement[] | undefined
onClose?: (result: any) => void
onChange?: (result: any) => void
}
export const dpstore = writable<IDatePopup>({
component: undefined,
currentDate: undefined,
anchor: undefined,
popup: undefined,
frendlyFocus: undefined,
onClose: undefined,
onChange: undefined
})
export function showDatePopup (
component: AnySvelteComponent | undefined,
currentDate: Date,
anchor?: HTMLElement,
popup?: HTMLElement,
frendlyFocus?: HTMLElement[] | undefined,
onClose?: (result: any) => void,
onChange?: (result: any) => void
): void {
dpstore.set({
component: component,
currentDate: currentDate,
anchor: anchor,
popup: popup,
frendlyFocus: frendlyFocus,
onClose: onClose,
onChange: onChange
})
}
export function closeDatePopup (): void {
dpstore.set({
component: undefined,
currentDate: undefined,
anchor: undefined,
popup: undefined,
frendlyFocus: undefined,
onClose: undefined,
onChange: undefined
})
}

View File

@ -16,7 +16,7 @@
<script lang="ts">
import { Event } from '@anticrm/calendar'
import { translate } from '@anticrm/platform'
import { DatePresenter } from '@anticrm/ui'
import { DateRangePresenter } from '@anticrm/ui'
import calendar from '../plugin'
export let value: Event
@ -47,10 +47,10 @@
<div class="antiSelect">
{#if date}
<DatePresenter value={date.getTime()} withTime={date.getMinutes() !== 0 && date.getHours() !== 0 && interval < DAY} />
<DateRangePresenter value={date.getTime()} withTime={date.getMinutes() !== 0 && date.getHours() !== 0 && interval < DAY} />
{#if interval > 0}
{#await formatDueDate(interval) then t}
<span class='ml-2 mr-1 whitespace-nowrap'>({t})</span>
{#await formatDueDate(interval) then t}
<span class='ml-2 mr-1 whitespace-nowrap'>({t})</span>
{/await}
{/if}
{:else}

View File

@ -16,7 +16,7 @@
<script lang="ts">
import { Reminder } from '@anticrm/calendar'
import { getResource } from '@anticrm/platform'
import { DateTimePresenter, showPanel, Tooltip } from '@anticrm/ui'
import { DateTimeRangePresenter, showPanel, Tooltip } from '@anticrm/ui'
import view from '@anticrm/view'
export let value: Reminder
@ -37,6 +37,6 @@
</Tooltip>
{/await}
</div>
<DateTimePresenter value={value.date + value.shift} />
<DateTimeRangePresenter value={value.date + value.shift} />
{/if}
</div>

View File

@ -22,7 +22,7 @@
import type { Candidate, Review } from '@anticrm/recruit'
import task, { SpaceWithStates } from '@anticrm/task'
import { StyledTextBox } from '@anticrm/text-editor'
import { DatePicker, EditBox, Grid, Row, Status as StatusControl } from '@anticrm/ui'
import ui, { DateRangePicker, Grid, Status as StatusControl, StylishEdit, EditBox, Row } from '@anticrm/ui'
import view from '@anticrm/view'
import { createEventDispatcher } from 'svelte'
import recruit from '../../plugin'
@ -172,8 +172,8 @@
{/if}
<EditBox label={recruit.string.Location} icon={recruit.icon.Location} bind:value={location} maxWidth={'13rem'} />
<OrganizationSelector bind:value={company} label={recruit.string.Company} />
<DatePicker title={recruit.string.StartDate} bind:value={startDate} withTime on:change={updateStart} />
<DatePicker title={recruit.string.DueDate} bind:value={dueDate} withTime />
<DateRangePicker title={recruit.string.StartDate} bind:value={startDate} withTime on:change={updateStart} />
<DateRangePicker title={recruit.string.DueDate} bind:value={dueDate} withTime />
<Row>
<StyledTextBox
emphasized

View File

@ -20,7 +20,7 @@
import { getClient, UserBox } from '@anticrm/presentation'
import { Issue, IssuePriority, IssueStatus, Team } from '@anticrm/tracker'
import { StyledTextBox } from '@anticrm/text-editor'
import { EditBox, Grid, Status as StatusControl, Button, showPopup } from '@anticrm/ui'
import ui, { EditBox, Grid, Status as StatusControl, Button, showPopup, DatePresenter, DateRangePresenter } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import tracker from '../plugin'
import { calcRank } from '../utils'
@ -88,6 +88,8 @@
await client.createDoc(tracker.class.Issue, _space, value, taskId)
}
let startDate: number | null = null
let targetDate: number | null = null
interface IPair {
icon: Asset
label: IntlString
@ -166,5 +168,7 @@
})
}}
/>
<DateRangePresenter value={startDate} labelNull={ui.string.StartDate} editable />
<DateRangePresenter value={targetDate} labelNull={ui.string.TargetDate} editable />
</div>
</Card>

View File

@ -17,7 +17,7 @@
<script lang="ts">
// import { TypeDate } from '@anticrm/core'
// import { IntlString } from '@anticrm/platform'
import { DatePresenter } from '@anticrm/ui'
import { DateRangePresenter } from '@anticrm/ui'
export let value: number | null | undefined
// export let label: IntlString
@ -25,4 +25,4 @@
// export let attributeType: TypeDate | undefined
</script>
<DatePresenter {value} on:change={onChange} editable />
<DateRangePresenter {value} withTime editable on:change={(res) => { if (res.detail !== undefined) onChange(res.detail) }} />

View File

@ -16,10 +16,10 @@
<script lang="ts">
// import { TypeDate } from '@anticrm/core'
import { DatePresenter } from '@anticrm/ui'
import { DateRangePresenter } from '@anticrm/ui'
export let value: number | null | undefined
// export let attributeType: TypeDate | undefined
</script>
<DatePresenter {value} />
<DateRangePresenter {value} />

View File

@ -33,6 +33,7 @@
PanelInstance,
Popup,
showPopup,
DatePickerPopup,
TooltipInstance
} from '@anticrm/ui'
import type { Application, NavigatorModel, SpecialNavModel, ViewConfiguration } from '@anticrm/workbench'
@ -366,6 +367,7 @@
<PanelInstance {contentPanel} />
<Popup />
<TooltipInstance />
<DatePickerPopup />
{:else}
No client
{/if}