Update DatePicker, DatePresenter. (#1221)

Signed-off-by: Alexander Platov <sas_lord@mail.ru>
This commit is contained in:
Alexander Platov 2022-03-29 17:29:53 +03:00 committed by GitHub
parent 24dfc7f274
commit 1700006c3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 938 additions and 719 deletions

View File

@ -41,7 +41,7 @@
const dispatch = createEventDispatcher()
</script>
<form class="antiCard" class:w-2125rem={size === 'small'} class:w-4125rem={size === 'medium'} on:submit|preventDefault={ () => {} }>
<form class="antiCard" class:w-85={size === 'small'} class:w-165={size === 'medium'} on:submit|preventDefault={ () => {} }>
<div class="antiCard-header">
<div class="antiCard-header__title"><Label {label} params={labelProps ?? {}} /></div>
{#if $$slots.error}

View File

@ -131,15 +131,12 @@
pointer-events: none;
user-select: none;
}
.emphasized .emphasized-focus + .label {
top: 0.5rem;
}
}
.emphasized {
padding: 1rem;
background-color: var(--theme-bg-accent-color);
border: 1px solid var(--theme-bg-accent-hover);
border-radius: 0.75rem;
border-radius: .5rem;
&.emphasized-focus {
background-color: var(--theme-bg-focused-color);
border-color: var(--theme-bg-focused-border);
@ -149,9 +146,5 @@
overflow: auto;
flex-grow: 1;
line-height: 150%;
.nolabel {
padding-top: 0;
}
}
</style>

View File

@ -49,6 +49,7 @@ input {
background-color: transparent;
outline: none;
color: var(--theme-caption-color);
&.wrong-input { background-color: var(--system-error-color) !important; }
}
audio, canvas, embed, iframe, img, object, svg, video {
display: block;
@ -108,10 +109,8 @@ p:last-child { margin-block-end: 0; }
.inline-flex { display: inline-flex; }
.flex-grow { flex-grow: 1; }
.flex-no-shrink { flex-shrink: 0; }
.flex-nowrap {
display: flex;
flex-wrap: nowrap;
}
.flex-wrap { flex-wrap: wrap; }
.flex-nowrap { flex-wrap: nowrap; }
.flex-baseline {
display: inline-flex;
align-items: baseline;
@ -141,9 +140,6 @@ p:last-child { margin-block-end: 0; }
align-items: stretch;
flex-wrap: nowrap;
}
.flex-wrap {
flex-wrap: wrap;
}
.flex-row-top {
display: flex;
align-items: flex-start;
@ -297,6 +293,8 @@ p:last-child { margin-block-end: 0; }
.mb-2 { margin-bottom: .5rem; }
.mb-3 { margin-bottom: .75rem; }
.mb-4 { margin-bottom: 1rem; }
.mx-1 { margin: 0 .125rem; }
.mx-2 { margin: 0 .25rem; }
.pr-1 { padding-right: .25rem; }
.pr-4 { padding-right: 1rem; }
@ -305,6 +303,7 @@ p:last-child { margin-block-end: 0; }
.p-10 { padding: 2.5rem; }
/* --------- */
.no-word-wrap { word-wrap: none; }
.relative { position: relative; }
.abs-lt-content {
position: absolute;
@ -344,8 +343,8 @@ p:last-child { margin-block-end: 0; }
.h-2 { height: .5rem; }
.h-9 { height: 2.25rem; }
.w-full { width: 100%; }
.w-2125rem {width: 21.25rem; }
.w-4125rem {width: 41.25rem; }
.w-85 {width: 21.25rem; }
.w-165 {width: 41.25rem; }
.min-w-0 { min-width: 0; }
.min-h-0 { min-height: 0; }
.clear-mins {
@ -433,6 +432,8 @@ a.no-line {
overflow: hidden;
white-space: pre-wrap;
width: max-content;
user-select: none;
min-width: auto;
}
.overflow-label {
white-space: nowrap;

View File

@ -466,26 +466,17 @@
& > .icon { color: var(--theme-caption-color); }
}
.label, .result {
.label {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
user-select: none;
min-width: 0;
}
.label {
font-size: .75rem;
font-weight: 500;
color: var(--theme-content-accent-color);
}
.result {
&.selected { color: var(--theme-caption-color); }
&.not-selected { color: var(--theme-content-dark-color); }
&.highlight {
font-weight: 500;
font-size: 1rem;
}
}
.group {
display: flex;
@ -494,20 +485,6 @@
margin-left: .75rem;
min-height: 0;
}
.divider {
margin: 0 .125rem;
font-weight: 500;
font-size: .75em;
color: var(--theme-content-dark-color);
&.inter { font-size: 1em; }
&.max {
margin: 0 .5rem;
font-size: .75rem;
color: var(--theme-content-trans-color);
}
}
}
/* Wraps */
@ -518,23 +495,48 @@
flex-wrap: nowrap;
min-width: 0;
&::after, &::before {
position: absolute;
width: 6px;
height: 6px;
background-color: var(--primary-button-enabled);
}
&::before {
top: -2px;
left: -4px;
clip-path: path('M0,6v-6h6v1h-5v5z');
}
&::after {
bottom: -2px;
right: -4px;
clip-path: path('M0,6h6v-6h-1v5h-5z');
&.conners {
&::after, &::before {
position: absolute;
width: 6px;
height: 6px;
background-color: var(--primary-button-enabled);
}
&::before {
top: -2px;
left: -4px;
clip-path: path('M0,6v-6h6v1h-5v5z');
}
&::after {
bottom: -2px;
right: -4px;
clip-path: path('M0,6h6v-6h-1v5h-5z');
}
}
&.wraped::before, &.wraped::after { content: ''; }
&.focusWI:focus-within::before, &.focusWI:focus-within::after { content: ''; }
&.focus:focus::before, &.focus:focus::after { content: ''; }
.result {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
user-select: none;
min-width: 0;
&.selected { color: var(--theme-caption-color); }
&.not-selected { color: var(--theme-content-dark-color); }
&.highlight {
font-weight: 500;
font-size: 1rem;
}
}
.divider {
font-weight: 500;
font-size: .75em;
color: var(--theme-content-dark-color);
&.inter { font-size: 1em; }
}
}

View File

@ -1,80 +0,0 @@
<!--
// 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 { onMount } from 'svelte'
import type { IntlString } from '@anticrm/platform'
import Calendar from './icons/Calendar.svelte'
import Close from './icons/Close.svelte'
import ui, { Label, DatePopup, DatePresenter, showPopup } from '..'
export let title: IntlString
export let value: Date | null | undefined = null
export let withTime: boolean = false
export let bigDay: boolean = false
export let show: boolean = false
const dispatch = createEventDispatcher()
let opened: boolean = false
let container: HTMLElement
let btn: HTMLElement
const changeValue = (result: any): void => {
if (result !== undefined) {
value = result
dispatch('change', result)
}
}
onMount(() => {
if (btn && show) {
btn.click()
show = false
}
})
</script>
<div class="antiSelect"
bind:this={container}
on:click|preventDefault={() => {
btn.focus()
if (!opened) {
opened = true
showPopup(DatePopup, { title, value, withTime }, container, (ev) => {
changeValue(ev)
opened = false
}, changeValue)
}
}}
>
<button
bind:this={btn}
class="button round-2"
class:selected={value}
>
<div class="icon">
{#if show}<Close size={'small'} />{:else}<Calendar size={'medium'} />{/if}
</div>
</button>
<div class="group">
<span class="label"><Label label={title} /></span>
{#if value != null}
<DatePresenter {value} withTime={withTime} {bigDay} wraped={opened} />
{:else}
<span class="result not-selected"><Label label={ui.string.NotSelected} /></span>
{/if}
</div>
</div>

View File

@ -1,335 +0,0 @@
<!--
// 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 { translate } from '@anticrm/platform'
import Back from './icons/Back.svelte'
import Forward from './icons/Forward.svelte'
import { createEventDispatcher } from 'svelte'
import ui, { Label, Button } from '..'
import type { TSelectDate, TCellStyle, ICell } from '../types'
export let title: IntlString
export let value: TSelectDate
export let withTime: boolean = false
const dispatch = createEventDispatcher()
const getNow = (): Date => {
const tempDate = new Date(Date.now())
return new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate())
}
const today: Date = getNow()
let todayString: string
async function todayStr () {
todayString = await translate(ui.string.Today, {})
}
todayStr()
let view: Date = (value === null || value === undefined) ? today : new Date(value)
const months: Array<string> = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
]
let monthYear: string
let days: Array<ICell> = []
let result: TSelectDate = value
let prevDiv: HTMLElement
let nextDiv: HTMLElement
let hourDiv: HTMLElement
let minDiv: HTMLElement
let okDiv: HTMLButtonElement
const daysInMonth = (date: Date): number => {
return 33 - new Date(date.getFullYear(), date.getMonth(), 33).getDate()
}
const compareDates = (d1: Date, d2: Date): boolean => {
if (d1.getFullYear() == d2.getFullYear() &&
d1.getMonth() == d2.getMonth() &&
d1.getDate() == d2.getDate()) return true
return false
}
const getDateStyle = (date: Date): TCellStyle => {
if (value !== undefined && value !== null && compareDates(value, date)) return 'selected'
return 'not-selected'
}
const renderCellStyles = (): void => {
days = []
for (let i = 1; i <= daysInMonth(view); i++) {
const tempDate = new Date(view.getFullYear(), view.getMonth(), i)
days.push({
dayOfWeek: (tempDate.getDay() === 0) ? 7 : tempDate.getDay(),
style: getDateStyle(tempDate),
today: compareDates(tempDate, today)
})
}
days = days
}
renderCellStyles()
$: monthYear = months[view.getMonth()] + ' ' + view.getFullYear()
$: if (value) renderCellStyles()
const zeroLead = (n: number): string => {
if (n < 10) return '0' + n.toString()
return n.toString()
}
const keyPress = (ev: KeyboardEvent, isHour: boolean): void => {
if (ev.key >= '0' && ev.key <= '9') {
const keyNumber: number = parseInt(ev.key, 10)
let number: number = isHour ? view.getHours() : view.getMinutes()
let newNumber: number = ((isHour && number > 2) || (!isHour && number > 5))
? keyNumber
: number * 10 + keyNumber
if (isHour) {
if (newNumber > 23) newNumber = 23
view.setHours(newNumber)
} else {
if (newNumber > 59) newNumber = 59
view.setMinutes(newNumber)
}
view = view
value = view
if (isHour && newNumber > 9) minDiv.focus()
if (!isHour && newNumber > 9) okDiv.focus()
}
}
const keyDown = (ev: KeyboardEvent, isHour: boolean): void => {
if (ev.key === 'Backspace') {
console.log('BACKSPACE')
if (isHour) view.setHours(0)
else view.setMinutes(0)
view = view
value = view
}
}
const navKey = (ev: KeyboardEvent): void => {
if (ev.code === 'ArrowLeft') prevDiv.click()
if (ev.code === 'ArrowRight') nextDiv.click()
}
</script>
<svelte:window on:keydown={navKey} />
<div class="popup">
<div class="flex-col caption-color">
<div class="title"><Label label={title} /></div>
<div class="flex-between nav">
<button
bind:this={prevDiv}
class="focused-button arrow"
on:click|preventDefault={() => {
view.setMonth(view.getMonth() - 1)
view = view
renderCellStyles()
}}><div class="icon"><Back size={'small'} /></div></button>
<div class="monthYear">
{monthYear}
</div>
<button
bind:this={nextDiv}
class="focused-button arrow"
on:click|preventDefault={() => {
view.setMonth(view.getMonth() + 1)
view = view
renderCellStyles()
}}><div class="icon"><Forward size={'small'} /></div></button>
</div>
</div>
<div class="calendar">
<div class="caption">Mo</div>
<div class="caption">Tu</div>
<div class="caption">We</div>
<div class="caption">Th</div>
<div class="caption">Fr</div>
<div class="caption">Sa</div>
<div class="caption">Su</div>
{#each days as day, i}
<div
class="day {day.style}"
class:today={day.today}
data-today={day.today ? todayString : ''}
style="grid-column: {day.dayOfWeek}/{day.dayOfWeek + 1};"
on:click={() => {
result = new Date(view.getFullYear(), view.getMonth(), i + 1, view.getHours(), view.getMinutes())
view = value = result
if (withTime) {
dispatch('update', result)
hourDiv.focus()
} else dispatch('close', result)
}}
>
{i + 1}
</div>
{/each}
</div>
{#if withTime}
<div class="calendar-divider" />
<div class="flex-row-center flex-reverse">
<div class="ml-2">
<Button bind:input={okDiv} label={ui.string.Ok} size={'small'} primary on:click={() => { dispatch('close', view) }} />
</div>
<div class="time-container">
<button
bind:this={hourDiv}
class="time-digit antiWrapper focus hours"
on:keypress={(ev) => { keyPress(ev, true) }}
on:keydown={(ev) => { keyDown(ev, true) }}
>
{zeroLead(view.getHours())}
</button>
<div class="time-divider">:</div>
<button
bind:this={minDiv}
class="time-digit antiWrapper focus minutes"
on:keypress={(ev) => { keyPress(ev, false) }}
on:keydown={(ev) => { keyDown(ev, false) }}
>
{zeroLead(view.getMinutes())}
</button>
</div>
</div>
{/if}
</div>
<style lang="scss">
.popup {
display: flex;
flex-direction: column;
padding: 1rem;
min-height: 0;
color: var(--theme-caption-color);
background-color: var(--theme-button-bg-focused);
border: 1px solid var(--theme-button-border-enabled);
border-radius: .75rem;
box-shadow: 0px 10px 20px rgba(0, 0, 0, .2);
user-select: none;
}
.arrow {
width: 2rem;
height: 2rem;
border: 1px solid var(--theme-bg-accent-color);
border-radius: .25rem;
}
.title {
margin-bottom: .75rem;
font-weight: 500;
text-align: left;
}
.nav {
min-width: 16.5rem;
.monthYear {
margin: 0 1rem;
line-height: 150%;
white-space: nowrap;
}
}
.calendar {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: .125rem;
margin-top: .5rem;
.caption, .day {
display: flex;
justify-content: center;
align-items: center;
width: 2.25rem;
height: 2.25rem;
color: var(--theme-content-dark-color);
}
.caption { font-size: .75rem; }
.day {
border-radius: .5rem;
border: 1px solid transparent;
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);
}
}
}
}
.calendar-divider {
flex-shrink: 0;
margin: .5rem 0;
height: 1px;
background-color: var(--theme-menu-divider);
}
.time-container {
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
min-width: 0;
height: 100%;
border: 1px solid var(--theme-button-border-enabled);
border-radius: .75rem;
}
.time-digit, .time-divider {
font-weight: 500;
font-size: 1.25rem;
cursor: pointer;
}
.time-divider {
margin: 0 .5rem;
font-size: 1rem;
color: var(--theme-content-dark-color);
}
.time-digit {
padding: 0;
font-weight: 600;
color: var(--theme-caption-color);
}
</style>

View File

@ -1,57 +0,0 @@
<!--
// 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 { getContext } from 'svelte'
import type { TSelectDate } from '../types'
export let value: TSelectDate
export let bigDay: boolean = false
export let wraped: boolean = false
export let withTime: boolean = false
const { currentLanguage } = getContext('lang')
const inter: boolean = (currentLanguage === 'ru') ?? false
const zeroLead = (n: number): string => {
if (n < 10) return '0' + n.toString()
return n.toString()
}
</script>
{#if value !== undefined}
<div class="antiWrapper" class:wraped>
<span class="result" class:selected={value !== null} class:not-selected={value === null} class:highlight={inter && bigDay}>
{#if value === null}--{:else}{inter ? value.getDate() : value.getMonth() + 1}{/if}
</span>
<span class="divider" class:inter>{inter ? '.' : '/'}</span>
<span class="result" class:selected={value !== null} class:not-selected={value === null} class:highlight={!inter && bigDay}>
{#if value === null}--{:else}{inter ? value.getMonth() + 1 : value.getDate()}{/if}
</span>
<span class="divider" class:inter>{inter ? '.' : '/'}</span>
<span class="result" class:selected={value !== null} class:not-selected={value === null}>
{#if value === null}--{:else}{value.getFullYear()}{/if}
</span>
{#if withTime}
<span class="divider max"> &mdash; </span>
<span class="result" class:selected={value !== null} class:not-selected={value === null} class:highlight={!inter && bigDay}>
{#if value === null}--{:else}{zeroLead(value.getHours())}{/if}
</span>
<span class="divider">:</span>
<span class="result" class:selected={value !== null} class:not-selected={value === null}>
{#if value === null}--{:else}{zeroLead(value.getMinutes())}{/if}
</span>
{/if}
</div>
{/if}

View File

@ -75,7 +75,7 @@
<Icon {icon} size={'small'}/>
</div>
{/if}
<div class="antiWrapper focusWI">
<div class="antiWrapper conners focusWI">
{#if format === 'password'}
<input bind:this={input} type='passsword' bind:value placeholder={phTraslate} {style} on:input={(ev) => ev.target && computeSize(ev.target)} on:change/>
{:else if format === 'number'}

View File

@ -22,6 +22,7 @@
export let component: AnySvelteComponent | AnyComponent | undefined = undefined
export let props: any | undefined = undefined
export let anchor: HTMLElement | undefined = undefined
export let onUpdate: ((result: any) => void) | undefined = undefined
export let fill = false
let triggerHTML: HTMLElement
@ -34,7 +35,7 @@
name={`tooltip-${label}`}
bind:this={triggerHTML}
on:mousemove={() => {
if (!shown) showTooltip(label, triggerHTML, direction, component, props, anchor)
if (!shown) showTooltip(label, triggerHTML, direction, component, props, anchor, onUpdate)
}}
>
<slot />

View File

@ -28,6 +28,7 @@
let clWidth: number
$: tooltipSW = !$tooltip.component
$: onUpdate = $tooltip.onUpdate
const fitTooltip = (): void => {
if (($tooltip.label || $tooltip.component) && tooltipHTML) {
@ -146,27 +147,15 @@
whileShow(ev)
}}
/>
<svg class="svg-mask">
<clipPath id="nub-bg"
><path
d="M7.3.6 4.2 4.3C2.9 5.4 1.5 6 0 6v1h18V6c-1.5 0-2.9-.6-4.2-1.7L10.7.6C9.9-.1 8.5-.2 7.5.4c0 .1-.1.1-.2.2z"
/></clipPath
>
<clipPath id="nub-border"
><path
d="M4.8 5.1 8 1.3s.1 0 .1-.1c.5-.3 1.4-.3 1.9.1L13.1 5l.1.1 1.2.9H18c-1.5 0-2.9-.6-4.2-1.7L10.7.6C9.9-.1 8.5-.2 7.5.4c0 .1-.1.1-.2.2L4.2 4.3C2.9 5.4 1.5 6 0 6h3.6l1.2-.9z"
/></clipPath
>
</svg>
{#if $tooltip.component}
<div class="popup-tooltip" bind:clientWidth={clWidth} bind:this={tooltipHTML}>
<div class="popup-tooltip antiPopup" bind:clientWidth={clWidth} bind:this={tooltipHTML}>
{#if $tooltip.label}<div class="fs-title mb-4">
<Label label={$tooltip.label} params={$tooltip.props ?? {}} />
</div>{/if}
{#if typeof $tooltip.component === 'string'}
<Component is={$tooltip.component} props={$tooltip.props}/>
<Component is={$tooltip.component} props={$tooltip.props} on:update={onUpdate !== undefined ? onUpdate : async () => {}} />
{:else}
<svelte:component this={$tooltip.component} {...$tooltip.props} />
<svelte:component this={$tooltip.component} {...$tooltip.props} on:update={onUpdate !== undefined ? onUpdate : async () => {}} />
{/if}
</div>
<div bind:this={nubHTML} class="nub {nubDirection ?? ''}" />

View File

@ -0,0 +1,39 @@
<!--
// 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 { Label, DatePresenter } from '../..'
export let title: IntlString
export let value: number | null | undefined = null
export let withTime: boolean = false
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>
<DatePresenter {value} {withTime} editable on:change={changeValue} />
</div>
</div>

View File

@ -0,0 +1,206 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { translate } from '@anticrm/platform'
import { afterUpdate, createEventDispatcher, onDestroy, onMount } from 'svelte'
import ui from '../..'
import type { TCellStyle, ICell } from './internal/DateUtils'
import { firstDay, day, getWeekDayName, areDatesEqual, getMonthName, daysInMonth } from './internal/DateUtils'
export let value: number | null | undefined
export let mondayStart: boolean = true
export let editable: boolean = false
const dispatch = createEventDispatcher()
let currentDate: Date = new Date(value ?? Date.now())
let days: Array<ICell> = []
let scrollDiv: HTMLElement
$: if (value) currentDate = new Date(value)
$: firstDayOfCurrentMonth = firstDay(currentDate, mondayStart)
const getNow = (): Date => {
const tempDate = new Date(Date.now())
return new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate())
}
const today: Date = getNow()
let todayString: string
translate(ui.string.Today, {}).then(res => todayString = res)
const getDateStyle = (date: Date): TCellStyle => {
if (value !== undefined && value !== null && areDatesEqual(currentDate, date)) return 'selected'
return 'not-selected'
}
const renderCellStyles = (): void => {
days = []
for (let i = 1; i <= daysInMonth(currentDate); i++) {
const tempDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), i)
days.push({
dayOfWeek: (tempDate.getDay() === 0) ? 7 : tempDate.getDay(),
style: getDateStyle(tempDate),
focused: false,
today: areDatesEqual(tempDate, today)
})
}
days = days
}
$: if (currentDate) renderCellStyles()
const scrolling = (ev: Event): void => {
console.log('!!! Scrolling:', ev)
}
afterUpdate(() => {
if (value) currentDate = new Date(value)
})
onMount(() => {
if (scrollDiv) scrollDiv.addEventListener('wheel', scrolling)
})
onDestroy(() => {
if (scrollDiv) scrollDiv.removeEventListener('wheel', scrolling)
})
</script>
<div bind:this={scrollDiv} class="convert-scroller">
<div class="popup">
<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}"
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)
value = currentDate.getTime()
dispatch('update', value)
}}
>
{i + 1}
</div>
{/each}
</div>
{/if}
</div>
</div>
<style lang="scss">
.convert-scroller {
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
min-width: 0;
min-height: 0;
width: 100%;
height: 100%;
border-radius: .5rem;
user-select: none;
overflow-x: scroll;
overflow-y: scroll;
// width: calc(100% - 1px);
// max-height: calc(100% - 1px);
// mask-image: linear-gradient(0deg, rgba(0, 0, 0, 0) 0, rgba(0, 0, 0, 1) 2rem, rgba(0, 0, 0, 1) calc(100% - 2rem), rgba(0, 0, 0, 0) 100%);
&::-webkit-scrollbar:vertical { width: 0; }
&::-webkit-scrollbar:horizontal { height: 0; }
}
.popup {
display: flex;
flex-direction: column;
min-height: 0;
width: 100%;
height: 100%;
// width: calc(100% + 1px);
// height: calc(100% + 1px);
color: var(--theme-caption-color);
border-radius: .5rem;
// pointer-events: none;
}
.monthYear {
margin: 0 1rem;
// line-height: 150%;
color: var(--theme-content-accent-color);
white-space: nowrap;
}
.calendar {
display: grid;
grid-template-columns: repeat(7, 1fr);
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: .25rem;
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; }
}
</style>

View File

@ -0,0 +1,415 @@
<!--
// 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 { onMount, createEventDispatcher, afterUpdate, onDestroy } from 'svelte'
import { daysInMonth } from './internal/DateUtils'
import { TimePopup, tooltipstore as tooltip, showTooltip } from '../..'
import DatePopup from './DatePopup.svelte'
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 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
}
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())
let selected: TEdits = 'day'
let dateDiv: HTMLElement
let timeDiv: HTMLElement
let dateBox: HTMLElement
let timeBox: HTMLElement
let dateContainer: HTMLElement
let text: HTMLElement
let dateShow: boolean = false
$: dateShow = !!($tooltip.label || $tooltip.component)
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'
}
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 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.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)
}
})
}
const saveDate = (): void => {
value = currentDate.getTime()
dateToEdits()
$tooltip.props = { value }
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)
}
const keyDown = (ev: KeyboardEvent, ed: TEdits): void => {
const target = ev.target as HTMLInputElement
const index = getIndex(ed)
if (ev.code === 'Backspace') {
target.value = ''
target.classList.remove('wrong-input')
}
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()
}
}
}
const updateFromTooltip = (result: any): void => {
if (result.detail !== undefined) {
currentDate = new Date(result.detail)
saveDate()
}
}
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 editClick = (ev: MouseEvent, el: TEdits): void => {
ev.stopPropagation()
const target = ev.target as HTMLInputElement
target.select()
}
onMount(() => { dateToEdits() })
// onDestroy(() => { closeTooltip() })
</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')}
>
<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)}
/>
</div>
{: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')}
</div>
{/if}
</div>
{/if}
</div>
{/if}
<style lang="scss">
.datetime-presenter-container {
position: relative;
display: flex;
align-items: center;
flex-wrap: nowrap;
flex-shrink: 0;
min-width: 0;
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;
border-radius: .25rem;
text-align: center;
transition: background-color .2s ease-out;
&:hover { background-color: var(--theme-bg-accent-hover); }
&:focus {
color: var(--theme-bg-color);
background-color: var(--primary-button-enabled);
}
&::placeholder { color: var(--theme-content-dark-color); }
}
.timeBox, .dateBox {
visibility: visible;
display: flex;
align-items: center;
min-width: 0;
max-width: 100%;
width: auto;
opacity: 1;
&.zero {
visibility: hidden;
width: 0;
max-width: 0;
opacity: 0;
}
}
.dateBox { margin-right: .25rem; }
</style>

View File

@ -13,12 +13,9 @@
// limitations under the License.
-->
<script lang="ts">
import type { TSelectDate } from '../types'
import DatePresenter from './DatePresenter.svelte'
export let value: TSelectDate
export let bigDay: boolean = false
export let wraped: boolean = false
export let value: number | null | undefined
</script>
<DatePresenter {value} {bigDay} {wraped} withTime />
<DatePresenter {value} withTime />

View File

@ -70,8 +70,11 @@
</div>
<style lang="scss">
.day-name,
.selected-month-controller {
.month-calendar {
user-select: none;
cursor: default;
}
.day-name {
display: flex;
justify-content: center;
}
@ -102,14 +105,9 @@
border: 1px solid var(--primary-button-focused-border);
background-color: var(--primary-button-enabled);
color: var(--primary-button-color);
cursor: pointer;
}
.wrongMonth {
color: var(--grayscale-grey-03);
}
.month-name {
font-size: 14px;
font-weight: bold;
margin: 0 5px;
color: var(--theme-content-dark-color);
}
</style>

View File

@ -0,0 +1,56 @@
<!--
// 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 { MILLISECONDS_IN_DAY, DAYS_IN_WEEK } from './internal/DateUtils'
export let value: number
const dispatch = createEventDispatcher()
interface ITimes {
label: string
value: number
}
let actions: Array<ITimes> = [{ label: '- week', value: -DAYS_IN_WEEK * MILLISECONDS_IN_DAY },
{ label: '- day', value: -MILLISECONDS_IN_DAY },
{ label: '- hour', value: -60 * 60 * 1000 },
{ label: '- 30 min', value: -30 * 60 * 1000 },
{ label: '- 5 min', value: -5 * 60 * 1000 },
{ label: '+ 5 min', value: 5 * 60 * 1000 },
{ label: '+ 30 min', value: 30 * 60 * 1000 },
{ label: '+ hour', value: 60 * 60 * 1000 },
{ label: '+ day', value: MILLISECONDS_IN_DAY },
{ label: '+ week', value: DAYS_IN_WEEK * MILLISECONDS_IN_DAY }]
</script>
<div class="scrollbox">
{#each actions as action}
<button class="ap-menuItem no-word-wrap" on:click={() => { dispatch('update', new Date(value + action.value)) }}>
{action.label}
</button>
{/each}
</div>
<style lang="scss">
.scrollbox {
overflow-x: hidden;
overflow-y: auto;
display: flex;
flex-direction: column;
}
</style>

View File

@ -54,15 +54,5 @@
display: grid;
grid-template-columns: repeat(4, 1fr);
border-collapse: collapse;
.row {
display: table-row;
}
.th {
display: table-cell;
}
.calendar {
display: table-cell;
padding: 0.3em;
}
}
</style>

View File

@ -0,0 +1,8 @@
<script lang="ts">
export let size: 'x-small' | 'small' | 'medium' | 'large' | 'full'
export let fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} viewBox="0 0 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>

View File

@ -0,0 +1,9 @@
<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 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12,1.2C6.1,1.2,1.2,6.1,1.2,12S6.1,22.8,12,22.8S22.8,17.9,22.8,12S17.9,1.2,12,1.2z M12,21.2c-5.1,0-9.2-4.2-9.2-9.2 S6.9,2.8,12,2.8s9.2,4.2,9.2,9.2S17.1,21.2,12,21.2z"/>
<path d="M16.1,14.5L13,12.7c-0.3-0.2-0.6-0.7-0.6-1.1V7.5c0-0.4-0.3-0.8-0.8-0.8c-0.4,0-0.8,0.3-0.8,0.8v4.1c0,0.9,0.6,1.9,1.3,2.4 l3.1,1.9c0.1,0.1,0.2,0.1,0.4,0.1c0.2,0,0.5-0.1,0.7-0.4C16.6,15.2,16.5,14.7,16.1,14.5z"/>
</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 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12,22C6.5,22,2,17.5,2,12C2,6.5,6.5,2,12,2c5.5,0,10,4.5,10,10C22,17.5,17.5,22,12,22z M12,4c-4.4,0-8,3.6-8,8 c0,4.4,3.6,8,8,8c4.4,0,8-3.6,8-8C20,7.6,16.4,4,12,4z M17,13h-6V7h2v4h4V13z"/>
</svg>

View File

@ -0,0 +1,9 @@
<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 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12.2,15.1l2.6-2.6c0.3-0.3,0.3-0.8,0-1.1L12.2,9c-0.3-0.3-0.8-0.3-1.1,0c-0.3,0.3-0.3,0.8,0,1.1l1.3,1.3H4 c-0.4,0-0.8,0.3-0.8,0.8c0,0.4,0.3,0.8,0.8,0.8h8.4l-1.3,1.3c-0.3,0.3-0.3,0.8,0,1.1c0.1,0.2,0.3,0.2,0.5,0.2S12.1,15.3,12.2,15.1z"/>
<path d="M12,3.2c-0.4,0-0.8,0.3-0.8,0.8s0.3,0.8,0.8,0.8c4.3,0,7.2,3,7.2,7.2s-3,7.2-7.2,7.2c-0.4,0-0.8,0.3-0.8,0.8 s0.3,0.8,0.8,0.8c5.2,0,8.8-3.6,8.8-8.8S17.2,3.2,12,3.2z"/>
</svg>

View File

@ -0,0 +1,9 @@
<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 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M20.5,11.5L18,9c-0.3-0.3-0.8-0.3-1.1,0c-0.3,0.3-0.3,0.8,0,1.1l1.3,1.3H9.8c-0.4,0-0.8,0.3-0.8,0.8 c0,0.4,0.3,0.8,0.8,0.8h8.4l-1.3,1.3c-0.3,0.3-0.3,0.8,0,1.1c0.2,0.2,0.3,0.2,0.5,0.2s0.4-0.1,0.5-0.2l2.6-2.6 C20.8,12.3,20.8,11.8,20.5,11.5z"/>
<path d="M11.8,19.2c-4.3,0-7.3-3-7.3-7.2s3-7.2,7.3-7.2c0.4,0,0.8-0.3,0.8-0.8s-0.3-0.8-0.8-0.8C6.6,3.2,3,6.8,3,12 s3.6,8.8,8.8,8.8c0.4,0,0.8-0.3,0.8-0.8S12.2,19.2,11.8,19.2z"/>
</svg>

View File

@ -11,9 +11,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
const DAYS_IN_WEEK = 7
const MILLISECONDS_IN_DAY = 86400000
const MILLISECONDS_IN_MINUTE = 60000
export const DAYS_IN_WEEK = 7
export const MILLISECONDS_IN_DAY = 86400000
export const MILLISECONDS_IN_MINUTE = 60000
export function firstDay (date: Date, mondayStart: boolean): Date {
const firstDayOfMonth = new Date(date)
@ -33,6 +33,15 @@ export function firstDay (date: Date, mondayStart: boolean): Date {
return result
}
export function getWeek (date: Date): number {
const onejan = new Date(date.getFullYear(), 0, 1)
return Math.ceil((((date.getTime() - onejan.getTime()) / MILLISECONDS_IN_DAY) + onejan.getDay() + 1) / DAYS_IN_WEEK)
}
export function daysInMonth (date: Date): number {
return 33 - new Date(date.getFullYear(), date.getMonth(), 33).getDate()
}
export function getWeekDayName (weekDay: Date, weekFormat: 'narrow' | 'short' | 'long' | undefined = 'short'): string {
const locale = new Intl.NumberFormat().resolvedOptions().locale
return new Intl.DateTimeFormat(locale, {
@ -67,6 +76,15 @@ export function getMonthName (date: Date): string {
const locale = new Intl.NumberFormat().resolvedOptions().locale
return new Intl.DateTimeFormat(locale, { month: 'long' }).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 {
d = new Date(d)
const day = d.getDay()

View File

@ -44,10 +44,11 @@ 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 DatePicker } from './components/DatePicker.svelte'
export { default as DatePopup } from './components/DatePopup.svelte'
export { default as DatePresenter } from './components/DatePresenter.svelte'
export { default as DateTimePresenter } from './components/DateTimePresenter.svelte'
export { default as DatePicker } from './components/calendar/DatePicker.svelte'
export { default as DatePopup } from './components/calendar/DatePopup.svelte'
export { default as TimePopup } from './components/calendar/TimePopup.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'
export { default as Grid } from './components/Grid.svelte'
export { default as Row } from './components/Row.svelte'

View File

@ -8,7 +8,8 @@ export const tooltipstore = writable<LabelAndProps>({
direction: undefined,
component: undefined,
props: undefined,
anchor: undefined
anchor: undefined,
onUpdate: undefined
})
export function showTooltip (
@ -17,7 +18,8 @@ export function showTooltip (
direction?: TooltipAligment,
component?: AnySvelteComponent | AnyComponent,
props?: any,
anchor?: HTMLElement
anchor?: HTMLElement,
onUpdate?: (result: any) => void
): void {
tooltipstore.set({
label: label,
@ -25,7 +27,8 @@ export function showTooltip (
direction: direction,
component: component,
props: props,
anchor: anchor
anchor: anchor,
onUpdate: onUpdate
})
}
@ -36,6 +39,7 @@ export function closeTooltip (): void {
direction: undefined,
component: undefined,
props: undefined,
anchor: undefined
anchor: undefined,
onUpdate: undefined
})
}

View File

@ -74,6 +74,7 @@ export interface LabelAndProps {
component?: AnySvelteComponent | AnyComponent
props?: any
anchor: HTMLElement | undefined
onUpdate?: (result: any) => void
}
export interface ListItem {
@ -86,11 +87,3 @@ export interface DropdownTextItem {
id: string
label: string
}
export type TSelectDate = Date | null | undefined
export type TCellStyle = 'not-selected' | 'selected'
export interface ICell {
dayOfWeek: number
style: TCellStyle
today?: boolean
}

View File

@ -47,7 +47,7 @@
<div class="antiSelect">
{#if date}
<DatePresenter value={date} withTime={date.getMinutes() !== 0 && date.getHours() !== 0 && interval < DAY} />
<DatePresenter 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>

View File

@ -26,7 +26,7 @@
let title: string = value.title
let description: string = value.description
let startDate: Date = new Date(value.date)
let startDate: number = value.date
let participants: Ref<Employee>[] = value.participants ?? []
const space = calendar.space.PersonalEvents
@ -39,7 +39,7 @@
async function saveReminder () {
await client.updateDoc(value._class, value.space, value._id, {
date: startDate.getTime(),
date: startDate,
description,
participants,
title
@ -56,7 +56,7 @@
label={calendar.string.EditReminder}
okAction={saveReminder}
okLabel={presentation.string.Save}
canSave={title.trim().length > 0 && startDate.getTime() > 0 && participants.length > 0}
canSave={title.trim().length > 0 && startDate > 0 && participants.length > 0}
{space}
on:close={() => {
dispatch('close')

View File

@ -16,7 +16,7 @@
import { Event } from '@anticrm/calendar'
import { Class, Doc, DocumentQuery, FindOptions, Ref } from '@anticrm/core'
import { Tooltip } from '@anticrm/ui'
import { addZero } from '@anticrm/ui/src/components/calendar/internal/DateUtils'
import { addZero } from '@anticrm/ui/src/components/calendar/internal/DateUtils'
import calendar from '../plugin'
import EventsPopup from './EventsPopup.svelte'

View File

@ -37,6 +37,6 @@
</Tooltip>
{/await}
</div>
<DateTimePresenter value={new Date(value.date + value.shift)} />
<DateTimePresenter value={value.date + value.shift} />
{/if}
</div>

View File

@ -37,7 +37,7 @@
{#await getEvent(tx.objectId) then event}
{#if event}
<span class="over-underline caption-color" on:click={() => { click(event) }}>{event.title}</span>&nbsp
<DateTimePresenter value={new Date(event.date)} />
<DateTimePresenter value={event.date} />
{/if}
{/await}
</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, Grid, Status as StatusControl, StylishEdit } from '@anticrm/ui'
import { DatePicker, Grid, Status as StatusControl, StylishEdit, EditBox, Row } from '@anticrm/ui'
import view from '@anticrm/view'
import { createEventDispatcher } from 'svelte'
import recruit from '../../plugin'
@ -38,8 +38,8 @@
let title: string = ''
let description: string = ''
let startDate: Date = new Date()
let dueDate: Date = new Date()
let startDate: number = Date.now()
let dueDate: number = Date.now()
let location: string = ''
let company: Ref<Organization> | undefined = undefined
@ -102,8 +102,8 @@
await client.addCollection(recruit.class.Review, doc.space, doc.attachedTo, doc.attachedToClass, 'reviews', {
number: (incResult as any).object.sequence,
date: startDate?.getTime() ?? null,
dueDate: dueDate?.getTime() ?? null,
date: startDate ?? null,
dueDate: dueDate ?? null,
description,
verdict: '',
title,
@ -133,6 +133,12 @@
}
$: validate(doc, doc._class)
const updateStart = (result: any): void => {
if (result.detail !== undefined) {
dueDate = result.detail
dueDate = dueDate
}
}
</script>
<Card
@ -152,39 +158,31 @@
>
<StatusControl slot="error" {status} />
<Grid column={1} rowGap={1.75}>
<Grid column={!preserveCandidate ? 2 : 1}>
<StylishEdit bind:value={title} label={recruit.string.Title} />
{#if !preserveCandidate}
<div class="antiComponentBox">
<UserBox
_class={contact.class.Person}
title={recruit.string.Candidate}
caption={recruit.string.Candidates}
bind:value={doc.attachedTo}
/>
</div>
{/if}
</Grid>
<StyledTextBox
emphasized
showButtons={false}
bind:content={description}
label={recruit.string.Description}
alwaysEdit
placeholder={recruit.string.AddDescription}
/>
<Grid column={2}>
<StylishEdit bind:value={location} label={recruit.string.Location} />
<div class="antiComponentBox">
<OrganizationSelector bind:value={company} label={recruit.string.Company} />
</div>
</Grid>
<div class="antiComponentBox">
<DatePicker title={recruit.string.StartDate} bind:value={startDate} withTime />
</div>
<div class="antiComponentBox">
<DatePicker title={recruit.string.DueDate} bind:value={dueDate} withTime />
</div>
<Grid column={2} rowGap={1.75}>
<EditBox label={recruit.string.Title} icon={recruit.icon.Review} bind:value={title} maxWidth={'13rem'} />
{#if !preserveCandidate}
<UserBox
_class={contact.class.Person}
title={recruit.string.Candidate}
caption={recruit.string.Candidates}
bind:value={doc.attachedTo}
/>
{:else}
<div></div>
{/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 />
<Row>
<StyledTextBox
emphasized
showButtons={false}
bind:content={description}
label={recruit.string.Description}
alwaysEdit
placeholder={recruit.string.AddDescription}
/>
</Row>
</Grid>
</Card>

View File

@ -106,18 +106,3 @@
on:change={() => client.update(object, { verdict: object.verdict })}
/>
{/if}
<style lang="scss">
.card {
align-self: stretch;
width: calc(50% - 3.5rem);
}
.arrows {
width: 4rem;
}
.description {
height: 10rem;
margin-bottom: 1rem;
}
</style>

View File

@ -21,7 +21,7 @@
import setting from '../../plugin'
export let tx: TxUpdateDoc<Integration>
export let doc: Integration
// export let doc: Integration
const client = getClient()

View File

@ -27,7 +27,7 @@
let name: string
const done = false
let dueTo: Date
let dueTo: number
$: _space = space
@ -48,7 +48,7 @@
{
name,
done,
dueTo: dueTo?.getTime() ?? undefined
dueTo: dueTo ?? undefined
}
)
}

View File

@ -24,7 +24,7 @@
export let item: TodoItem
let name: string = ''
let dueTo: Date | undefined | null = null
let dueTo: number | null | undefined = null
let _itemId: Ref<TodoItem>
@ -32,7 +32,7 @@
_itemId = item._id
name = item.name
if (item.dueTo != null) {
dueTo = new Date(item.dueTo)
dueTo = item.dueTo
} else {
dueTo = null
}
@ -51,7 +51,7 @@
ops.name = name
}
if (item.dueTo !== dueTo) {
ops.dueTo = (dueTo?.getTime() ?? null) as unknown as Timestamp
ops.dueTo = (dueTo ?? null) as unknown as Timestamp
}
if (Object.keys(ops).length === 0) {

View File

@ -24,7 +24,7 @@
$: todos = (value.$lookup?.todoItems as TodoItem[]) ?? []
</script>
<div class="applications-container">
<div class="flex-col">
{#if todos.length > 0}
<Table
_class={task.class.TodoItem}
@ -38,17 +38,3 @@
/>
{/if}
</div>
<style lang="scss">
.applications-container {
display: flex;
flex-direction: column;
.title {
margin-right: 0.75rem;
font-weight: 500;
font-size: 1.25rem;
color: var(--theme-caption-color);
}
}
</style>

View File

@ -15,34 +15,14 @@
-->
<script lang="ts">
import { TypeDate } from '@anticrm/core'
import { IntlString } from '@anticrm/platform'
import { DatePopup, showPopup } from '@anticrm/ui'
import DatePresenter from './DatePresenter.svelte'
// import { TypeDate } from '@anticrm/core'
// import { IntlString } from '@anticrm/platform'
import { DatePresenter } from '@anticrm/ui'
export let value: number | Date | undefined
export let label: IntlString
export let value: number | null | undefined
// export let label: IntlString
export let onChange: (value: any) => void
export let attributeType: TypeDate | undefined
$: date = value ? new Date(value) : new Date()
let container: HTMLElement
let opened: boolean = false
// export let attributeType: TypeDate | undefined
</script>
<div class="flex-row-center" bind:this={container}
on:click|preventDefault={() => {
if (!opened) {
opened = true
showPopup(DatePopup, { value: date, title: label, withTime: attributeType?.withTime ?? false }, container, (result) => {
if (result) {
value = result.getTime()
onChange(value)
}
opened = false
})
}
}} >
<DatePresenter {value} {attributeType} />
</div>
<DatePresenter {value} on:change={onChange} editable />

View File

@ -15,19 +15,11 @@
-->
<script lang="ts">
import { TypeDate } from '@anticrm/core'
// import { TypeDate } from '@anticrm/core'
import { DatePresenter } from '@anticrm/ui'
export let value: number | Date | undefined
export let attributeType: TypeDate | undefined
$: date = value ? new Date(value) : undefined
export let value: number | null | undefined
// export let attributeType: TypeDate | undefined
</script>
<div class="antiSelect">
{#if date}
<DatePresenter value={date} withTime={attributeType?.withTime ?? false} />
{:else}
No date
{/if}
</div>
<DatePresenter {value} />

View File

@ -213,13 +213,17 @@
{#if client}
<svg class="svg-mask">
<clipPath id="notify-normal">
<path
d="M0,0v52.5h52.5V0H0z M34,23.2c-3.2,0-5.8-2.6-5.8-5.8c0-3.2,2.6-5.8,5.8-5.8c3.2,0,5.8,2.6,5.8,5.8 C39.8,20.7,37.2,23.2,34,23.2z"
/>
<path d="M0,0v52.5h52.5V0H0z M34,23.2c-3.2,0-5.8-2.6-5.8-5.8c0-3.2,2.6-5.8,5.8-5.8c3.2,0,5.8,2.6,5.8,5.8 C39.8,20.7,37.2,23.2,34,23.2z" />
</clipPath>
<clipPath id="notify-small">
<path d="M0,0v45h45V0H0z M29.5,20c-2.8,0-5-2.2-5-5s2.2-5,5-5s5,2.2,5,5S32.3,20,29.5,20z" />
</clipPath>
<clipPath id="nub-bg">
<path d="M7.3.6 4.2 4.3C2.9 5.4 1.5 6 0 6v1h18V6c-1.5 0-2.9-.6-4.2-1.7L10.7.6C9.9-.1 8.5-.2 7.5.4c0 .1-.1.1-.2.2z" />
</clipPath>
<clipPath id="nub-border">
<path d="M4.8 5.1 8 1.3s.1 0 .1-.1c.5-.3 1.4-.3 1.9.1L13.1 5l.1.1 1.2.9H18c-1.5 0-2.9-.6-4.2-1.7L10.7.6C9.9-.1 8.5-.2 7.5.4c0 .1-.1.1-.2.2L4.2 4.3C2.9 5.4 1.5 6 0 6h3.6l1.2-.9z" />
</clipPath>
</svg>
<div class="workbench-container">
<div class="antiPanel-application" on:click={toggleNav}>

View File

@ -142,17 +142,17 @@ test.describe('recruit tests', () => {
// Click button:has-text("Review")
await page.click('button:has-text("Review")')
// Click [placeholder="\ "]
await page.click('[placeholder="\\ "]')
await page.click('[placeholder="placeholder"]')
// Fill [placeholder="\ "]
await page.fill('[placeholder="\\ "]', 'Meet PEterson')
await page.fill('[placeholder="placeholder"]', 'Meet PEterson')
// Click text=Location Company Company >> [placeholder="\ "]
await page.click('text=Location Company Company >> [placeholder="\\ "]')
await page.click('text=placeholder Location >> [placeholder="placeholder"]')
// Fill text=Location Company Company >> [placeholder="\ "]
await page.fill('text=Location Company Company >> [placeholder="\\ "]', 'NSK')
await page.fill('text=placeholder Location >> [placeholder="placeholder"]', 'NSK')
// Click text=Company Company >> div
await page.click('text=Company Company >> div')
// await page.click('text=Company Company >> div')
// Click button:has-text("Apple")
await page.click('button:has-text("Apple")')
// await page.click('button:has-text("Apple")')
// Click text=Candidate Not selected >> span
await page.click('text=Candidate Not selected >> span')
// Click button:has-text("Andrey P.")