123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
- <script setup lang="ts">
- import VueDatePicker from '@vuepic/vue-datepicker'
- import { useEventListener } from '@vueuse/core'
- import { computed, nextTick, ref, toRef } from 'vue'
- import useValue from '#shared/components/Form/composables/useValue.ts'
- import type { DateTimeContext } from '#shared/components/Form/fields/FieldDate/types.ts'
- import { useDateTime } from '#shared/components/Form/fields/FieldDate/useDateTime.ts'
- import { i18n } from '#shared/i18n.ts'
- import testFlags from '#shared/utils/testFlags.ts'
- import '@vuepic/vue-datepicker/dist/main.css'
- interface Props {
- context: DateTimeContext
- }
- const props = defineProps<Props>()
- const contextReactive = toRef(props, 'context')
- const { localValue } = useValue(contextReactive)
- const {
- ariaLabels,
- displayFormat,
- is24,
- minDate,
- position,
- timePicker,
- valueFormat,
- } = useDateTime(contextReactive)
- const config = {
- keepActionRow: true,
- }
- const actionRow = {
- showSelect: false,
- showCancel: false,
- showNow: true,
- showPreview: false,
- }
- const input = ref<HTMLInputElement>()
- const picker = ref()
- const showPicker = ref(false)
- const pickerDisplayStyle = computed(() => (showPicker.value ? 'block' : 'none'))
- const expandPicker = () => {
- showPicker.value = true
- nextTick(() => {
- testFlags.set(`field-date-time-${props.context.id}.opened`)
- })
- }
- const collapsePicker = () => {
- showPicker.value = false
- nextTick(() => {
- testFlags.set(`field-date-time-${props.context.id}.closed`)
- })
- }
- // Hide calendar, if clicked outside of the picker or input.
- useEventListener('click', (e) => {
- const { target } = e
- if (!target || !picker.value || !showPicker.value || !input.value) return
- const outer = (target as Element).closest('.formkit-outer')
- if (!outer) return
- const insideFormField = !outer.contains(target as Node)
- if (insideFormField) return
- collapsePicker()
- })
- </script>
- <template>
- <div class="flex w-full">
- <!-- eslint-disable vuejs-accessibility/aria-props -->
- <VueDatePicker
- ref="picker"
- v-model="localValue"
- :class="{ 'pointer-events-none': context.disabled }"
- :uid="context.id"
- :model-type="valueFormat"
- :name="context.node.name"
- :clearable="!!context.clearable"
- :disabled="context.disabled"
- :range="context.range"
- :enable-time-picker="timePicker"
- :format="displayFormat"
- :is-24="is24"
- :locale="i18n.locale()"
- :max-date="context.maxDate"
- :min-date="minDate"
- :start-date="minDate || context.maxDate"
- :ignore-time-validation="!timePicker"
- :prevent-min-max-navigation="
- Boolean(minDate || context.maxDate || context.futureOnly)
- "
- :now-button-label="$t('Today')"
- :position="position"
- :action-row="actionRow"
- :config="config"
- :aria-labels="ariaLabels"
- :inline="{ input: true }"
- :month-change-on-scroll="false"
- :text-input="{ openMenu: 'toggle' }"
- auto-apply
- dark
- @open="expandPicker"
- @close="collapsePicker"
- @blur="context.handlers.blur"
- >
- <template
- #dp-input="{
- value,
- onInput,
- onEnter,
- onTab,
- onBlur,
- onKeypress,
- onPaste,
- }"
- >
- <input
- :id="context.id"
- ref="input"
- :value="value"
- :name="context.node.name"
- :class="context.classes.input"
- :aria-describedby="context.describedBy"
- :disabled="context.disabled"
- type="text"
- v-bind="context.attrs"
- @input="onInput"
- @keypress.enter="onEnter"
- @keypress.tab="onTab"
- @keypress="onKeypress"
- @paste="onPaste"
- @blur="onBlur"
- @focus="expandPicker"
- />
- <div v-if="showPicker" class="w-full" :class="{ 'pe-2': context.link }">
- <div class="h-[1px] w-full bg-white/10"></div>
- </div>
- </template>
- <template #clear-icon>
- <CommonIcon
- class="text-gray absolute -mt-5 shrink-0 ltr:right-2 rtl:left-2"
- :aria-label="i18n.t('Clear Selection')"
- name="close-small"
- size="base"
- role="button"
- tabindex="0"
- @click.stop="picker?.clearValue()"
- @keypress.space.prevent.stop="picker?.clearValue()"
- />
- </template>
- <template #clock-icon>
- <CommonIcon name="clock" size="tiny" decorative />
- </template>
- <template #calendar-icon>
- <CommonIcon name="calendar" size="tiny" decorative />
- </template>
- <template #arrow-left>
- <CommonIcon name="chevron-left" size="xs" decorative />
- </template>
- <template #arrow-right>
- <CommonIcon name="chevron-right" size="xs" decorative />
- </template>
- <template #arrow-up>
- <CommonIcon name="chevron-up" size="xs" decorative />
- </template>
- <template #arrow-down>
- <CommonIcon name="chevron-down" size="xs" decorative />
- </template>
- </VueDatePicker>
- </div>
- </template>
- <style scoped>
- :deep(.dp__outer_menu_wrap) .dp__menu {
- /* stylelint-disable value-keyword-case */
- display: v-bind(pickerDisplayStyle);
- max-width: var(--dp-menu-min-width);
- margin: 0 auto;
- }
- :deep(.dp__theme_dark) {
- --dp-background-color: theme(colors.gray.500);
- --dp-text-color: theme(colors.white);
- --dp-hover-color: theme(colors.transparent);
- --dp-hover-text-color: theme(colors.white);
- --dp-hover-icon-color: theme(colors.white);
- --dp-primary-color: theme(colors.blue.DEFAULT);
- --dp-secondary-color: theme(colors.gray.200);
- --dp-border-color: theme(colors.transparent);
- --dp-menu-border-color: theme(colors.transparent);
- --dp-border-color-hover: theme(colors.transparent);
- --dp-range-between-dates-background-color: theme(colors.blue.highlight);
- --dp-range-between-dates-text-color: theme(colors.white);
- --dp-range-between-border-color: theme(colors.transparent);
- &:where([data-errors='true'] *),
- &:where([data-invalid='true'] *) {
- --dp-background-color: theme(colors.red.dark);
- }
- }
- :deep(.dp__main) {
- --dp-font-family: theme(fontFamily.sans);
- --dp-border-radius: theme(borderRadius.md);
- --dp-cell-border-radius: theme(borderRadius.full);
- --dp-button-height: theme(size.8);
- --dp-action-button-height: theme(size.8);
- --dp-month-year-row-height: theme(size.8);
- --dp-month-year-row-button-size: theme(size.8);
- --dp-common-padding: theme(padding.2);
- --dp-action-row-padding: theme(padding.2);
- --dp-menu-min-width: 260px;
- --dp-font-size: theme(fontSize.base);
- --dp-preview-font-size: theme(fontSize.base);
- --dp-time-font-size: theme(fontSize.xl);
- & > div {
- width: 100%;
- }
- .dp {
- &__button,
- &__action_button {
- border: none;
- color: theme(colors.white);
- background: theme(colors.gray.200);
- }
- &--clear-btn {
- top: 2.3rem;
- }
- &--tp-wrap {
- padding: var(--dp-common-padding);
- max-width: none;
- }
- &__btn,
- &__button,
- &__calendar_item,
- &__action_button {
- transition: none;
- border-radius: theme(borderRadius.md);
- }
- &__action_buttons {
- margin-inline-start: 0;
- flex-grow: 1;
- }
- &__action_button {
- margin-inline-start: 0;
- transition: none;
- flex-grow: 1;
- display: inline-flex;
- justify-content: center;
- border-radius: theme(borderRadius.md);
- }
- &__action_cancel {
- border: none;
- }
- &--arrow-btn-nav .dp__inner_nav {
- color: theme(colors.blue.DEFAULT);
- }
- &__overlay_container {
- padding-bottom: theme(padding.2);
- }
- &__overlay_container + .dp__button,
- &__overlay_row + .dp__button {
- width: auto;
- margin: theme(margin.2);
- }
- &__overlay_container + .dp__button:not(.dp__overlay_action) {
- width: calc(var(--dp-menu-min-width) - theme(margin[1.5]) * 2);
- }
- &__overlay_container + .dp__button.dp__overlay_action {
- width: calc(var(--dp-menu-min-width) - theme(margin[2.5]) * 2);
- }
- }
- }
- </style>
|