mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 11:42:30 +03:00
Update DataPicker layout (#1309)
Signed-off-by: Alexander Platov <sas_lord@mail.ru>
This commit is contained in:
parent
424107312d
commit
30976696f9
@ -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;
|
||||
|
@ -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}}",
|
||||
|
@ -15,6 +15,10 @@
|
||||
"None": "Нет",
|
||||
"NotSelected": "Не выбрано",
|
||||
"Today": "Сегодня",
|
||||
"NoDate": "Без даты",
|
||||
"StartDate": "Дата начала",
|
||||
"TargetDate": "Дата окончания",
|
||||
"Overdue": "Просрочено",
|
||||
"English": "Английский",
|
||||
"Russian": "Русский",
|
||||
"MinutesBefore": "{minutes, plural, =1 {за минуту} other {за # минут}}",
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
129
packages/ui/src/components/calendar/DatePickerPopup.svelte
Normal file
129
packages/ui/src/components/calendar/DatePickerPopup.svelte
Normal 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>
|
@ -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)
|
||||
|
@ -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(' ', ' ')
|
||||
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()
|
||||
if (value !== null && value !== undefined) dateToEdits()
|
||||
else if (value === null) {
|
||||
edits.map(edit => edit.value = -1)
|
||||
currentDate = today
|
||||
}
|
||||
|
||||
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()
|
||||
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)
|
||||
}
|
||||
computeSize(t, ed)
|
||||
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>
|
||||
|
44
packages/ui/src/components/calendar/DateRangePicker.svelte
Normal file
44
packages/ui/src/components/calendar/DateRangePicker.svelte
Normal 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>
|
210
packages/ui/src/components/calendar/DateRangePopup.svelte
Normal file
210
packages/ui/src/components/calendar/DateRangePopup.svelte
Normal 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>
|
474
packages/ui/src/components/calendar/DateRangePresenter.svelte
Normal file
474
packages/ui/src/components/calendar/DateRangePresenter.svelte
Normal 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>
|
@ -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} />
|
||||
|
@ -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} />
|
@ -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>
|
||||
|
@ -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>
|
@ -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 {
|
||||
|
8
packages/ui/src/components/icons/NavNext.svelte
Normal file
8
packages/ui/src/components/icons/NavNext.svelte
Normal 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>
|
8
packages/ui/src/components/icons/NavPrev.svelte
Normal file
8
packages/ui/src/components/icons/NavPrev.svelte
Normal 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>
|
@ -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'
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
|
@ -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,9 +47,9 @@
|
||||
|
||||
<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}
|
||||
{#await formatDueDate(interval) then t}
|
||||
<span class='ml-2 mr-1 whitespace-nowrap'>({t})</span>
|
||||
{/await}
|
||||
{/if}
|
||||
|
@ -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>
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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) }} />
|
||||
|
@ -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} />
|
||||
|
@ -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}
|
||||
|
Loading…
Reference in New Issue
Block a user