FieldDateTimeInput.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import VueDatePicker, { type DatePickerInstance } from '@vuepic/vue-datepicker'
  4. import { storeToRefs } from 'pinia'
  5. import { computed, ref, toRef } from 'vue'
  6. import useValue from '#shared/components/Form/composables/useValue.ts'
  7. import type { DateTimeContext } from '#shared/components/Form/fields/FieldDate/types.ts'
  8. import { useDateTime } from '#shared/components/Form/fields/FieldDate/useDateTime.ts'
  9. import { EnumTextDirection } from '#shared/graphql/types.ts'
  10. import { i18n } from '#shared/i18n.ts'
  11. import { useThemeStore } from '#desktop/stores/theme.ts'
  12. import '@vuepic/vue-datepicker/dist/main.css'
  13. interface Props {
  14. context: DateTimeContext
  15. }
  16. const props = defineProps<Props>()
  17. const contextReactive = toRef(props, 'context')
  18. const { localValue } = useValue(contextReactive)
  19. const {
  20. ariaLabels,
  21. displayFormat,
  22. is24,
  23. localeStore,
  24. minDate,
  25. position,
  26. timePicker,
  27. valueFormat,
  28. } = useDateTime(contextReactive)
  29. const config = computed(() => ({
  30. keepActionRow: true,
  31. arrowLeft:
  32. localeStore.localeData?.dir === EnumTextDirection.Rtl
  33. ? 'calc(100% - 17px)'
  34. : '17px',
  35. }))
  36. const actionRow = computed(() => ({
  37. showSelect: false,
  38. showCancel: false,
  39. // Do not show 'Today' for range selection, because it will close the picker
  40. // even if only one date was selected.
  41. showNow: !props.context.range,
  42. showPreview: false,
  43. }))
  44. const inputIcon = computed(() => {
  45. if (contextReactive.value.range) return 'calendar-range'
  46. if (timePicker.value) return 'calendar-date-time'
  47. return 'calendar-event'
  48. })
  49. const picker = ref<DatePickerInstance>()
  50. const { currentTheme } = storeToRefs(useThemeStore())
  51. const dark = computed(() => currentTheme.value === 'dark')
  52. </script>
  53. <template>
  54. <div class="w-full">
  55. <!-- eslint-disable vuejs-accessibility/aria-props -->
  56. <VueDatePicker
  57. ref="picker"
  58. v-model="localValue"
  59. :uid="context.id"
  60. :model-type="valueFormat"
  61. :name="context.node.name"
  62. :clearable="context.clearable ?? false"
  63. :disabled="context.disabled"
  64. :range="context.range"
  65. :enable-time-picker="timePicker"
  66. :format="displayFormat"
  67. :is-24="is24"
  68. :dark="dark"
  69. :locale="i18n.locale()"
  70. :max-date="context.maxDate"
  71. :min-date="minDate"
  72. :start-date="minDate || context.maxDate"
  73. :ignore-time-validation="!timePicker"
  74. :prevent-min-max-navigation="
  75. Boolean(minDate || context.maxDate || context.futureOnly)
  76. "
  77. :now-button-label="$t('Today')"
  78. :position="position"
  79. :action-row="actionRow"
  80. :config="config"
  81. :aria-labels="ariaLabels"
  82. auto-apply
  83. text-input
  84. offset="12"
  85. @blur="context.handlers.blur"
  86. >
  87. <template
  88. #dp-input="{
  89. value,
  90. onInput,
  91. onEnter,
  92. onTab,
  93. onBlur,
  94. onKeypress,
  95. onPaste,
  96. }"
  97. >
  98. <input
  99. :id="context.id"
  100. :value="value"
  101. :name="context.node.name"
  102. :class="context.classes.input"
  103. :disabled="context.disabled"
  104. :aria-describedby="context.describedBy"
  105. v-bind="context.attrs"
  106. type="text"
  107. @input="onInput"
  108. @keypress.enter="onEnter"
  109. @keypress.tab="onTab"
  110. @keypress="onKeypress"
  111. @paste="onPaste"
  112. @blur="onBlur"
  113. />
  114. </template>
  115. <template #input-icon>
  116. <CommonIcon :name="inputIcon" size="tiny" decorative />
  117. </template>
  118. <template #clear-icon>
  119. <CommonIcon
  120. class="me-3"
  121. name="x-lg"
  122. size="xs"
  123. tabindex="0"
  124. role="button"
  125. :aria-label="$t('Clear Selection')"
  126. @click.stop="picker?.clearValue()"
  127. />
  128. </template>
  129. <template #clock-icon>
  130. <CommonIcon name="clock" size="tiny" decorative />
  131. </template>
  132. <template #calendar-icon>
  133. <CommonIcon name="calendar" size="tiny" decorative />
  134. </template>
  135. <template #arrow-left>
  136. <CommonIcon name="chevron-left" size="xs" decorative />
  137. </template>
  138. <template #arrow-right>
  139. <CommonIcon name="chevron-right" size="xs" decorative />
  140. </template>
  141. <template #arrow-up>
  142. <CommonIcon name="chevron-up" size="xs" decorative />
  143. </template>
  144. <template #arrow-down>
  145. <CommonIcon name="chevron-down" size="xs" decorative />
  146. </template>
  147. </VueDatePicker>
  148. </div>
  149. </template>
  150. <style scoped>
  151. :deep(.dp__theme_light) {
  152. --dp-background-color: theme(colors.white);
  153. --dp-text-color: theme(colors.black);
  154. --dp-hover-color: theme(colors.blue.600);
  155. --dp-hover-text-color: theme(colors.black);
  156. --dp-hover-icon-color: theme(colors.blue.800);
  157. --dp-primary-color: theme(colors.blue.800);
  158. --dp-primary-disabled-color: theme(colors.blue.500);
  159. --dp-primary-text-color: theme(colors.white);
  160. --dp-secondary-color: theme(colors.stone.200);
  161. --dp-border-color: theme(colors.transparent);
  162. --dp-menu-border-color: theme(colors.neutral.100);
  163. --dp-border-color-hover: theme(colors.transparent);
  164. --dp-disabled-color: theme(colors.transparent);
  165. --dp-disabled-color-text: theme(colors.stone.200);
  166. --dp-scroll-bar-background: theme(colors.blue.200);
  167. --dp-scroll-bar-color: theme(colors.stone.200);
  168. --dp-success-color: theme(colors.green.500);
  169. --dp-success-color-disabled: theme(colors.green.300);
  170. --dp-icon-color: theme(colors.stone.200);
  171. --dp-danger-color: theme(colors.red.500);
  172. --dp-marker-color: theme(colors.blue.600);
  173. --dp-tooltip-color: theme(colors.blue.200);
  174. --dp-highlight-color: theme(colors.blue.800);
  175. --dp-range-between-dates-background-color: theme(colors.blue.500);
  176. --dp-range-between-dates-text-color: theme(colors.blue.800);
  177. --dp-range-between-border-color: theme(colors.neutral.100);
  178. --dp-input-background-color: theme(colors.blue.200);
  179. .dp {
  180. &__clear_icon:hover {
  181. color: theme(colors.black);
  182. }
  183. &__btn,
  184. &__calendar_item,
  185. &__action_button {
  186. &:hover {
  187. outline-color: theme(colors.blue.600);
  188. }
  189. &:focus {
  190. outline-color: theme(colors.blue.800);
  191. }
  192. }
  193. &__button,
  194. &__action_button {
  195. color: theme(colors.gray.300);
  196. background: theme(colors.green.200);
  197. }
  198. }
  199. }
  200. :deep(.dp__theme_dark) {
  201. --dp-background-color: theme(colors.gray.500);
  202. --dp-text-color: theme(colors.white);
  203. --dp-hover-color: theme(colors.blue.900);
  204. --dp-hover-text-color: theme(colors.white);
  205. --dp-hover-icon-color: theme(colors.blue.800);
  206. --dp-primary-color: theme(colors.blue.800);
  207. --dp-primary-disabled-color: theme(colors.blue.950);
  208. --dp-primary-text-color: theme(colors.white);
  209. --dp-secondary-color: theme(colors.neutral.500);
  210. --dp-border-color: theme(colors.transparent);
  211. --dp-menu-border-color: theme(colors.gray.900);
  212. --dp-border-color-hover: theme(colors.transparent);
  213. --dp-disabled-color: theme(colors.transparent);
  214. --dp-disabled-color-text: theme(colors.neutral.500);
  215. --dp-scroll-bar-background: theme(colors.gray.700);
  216. --dp-scroll-bar-color: theme(colors.gray.400);
  217. --dp-success-color: theme(colors.green.500);
  218. --dp-success-color-disabled: theme(colors.green.900);
  219. --dp-icon-color: theme(colors.neutral.500);
  220. --dp-danger-color: theme(colors.red.500);
  221. --dp-marker-color: theme(colors.blue.700);
  222. --dp-tooltip-color: theme(colors.gray.700);
  223. --dp-highlight-color: theme(colors.blue.800);
  224. --dp-range-between-dates-background-color: theme(colors.blue.950);
  225. --dp-range-between-dates-text-color: theme(colors.blue.800);
  226. --dp-range-between-border-color: theme(colors.gray.900);
  227. --dp-input-background-color: theme(colors.gray.700);
  228. .dp {
  229. &__clear_icon:hover {
  230. color: theme(colors.white);
  231. }
  232. &__btn,
  233. &__calendar_item,
  234. &__action_button {
  235. &:hover {
  236. outline-color: theme(colors.blue.900);
  237. }
  238. &:focus {
  239. outline-color: theme(colors.blue.800);
  240. }
  241. }
  242. &__button,
  243. &__action_button {
  244. color: theme(colors.neutral.400);
  245. background: theme(colors.gray.600);
  246. }
  247. }
  248. }
  249. :deep(.dp__main) {
  250. /* stylelint-disable value-keyword-case */
  251. --dp-font-family: theme(fontFamily.sans);
  252. --dp-border-radius: theme(borderRadius.lg);
  253. --dp-cell-border-radius: theme(borderRadius.md);
  254. --dp-button-height: theme(size.6);
  255. --dp-month-year-row-height: theme(size.7);
  256. --dp-month-year-row-button-size: theme(size.7);
  257. --dp-button-icon-height: theme(height.4);
  258. --dp-cell-size: theme(size.8);
  259. --dp-cell-padding: theme(padding.2);
  260. --dp-common-padding: theme(padding.2);
  261. --dp-input-icon-padding: theme(padding.2);
  262. --dp-input-padding: var(--dp-common-padding);
  263. --dp-menu-min-width: 260px;
  264. --dp-action-buttons-padding: theme(padding.3);
  265. --dp-row-margin: theme(margin.2) theme(margin.0);
  266. --dp-calendar-header-cell-padding: theme(padding.2);
  267. --dp-two-calendars-spacing: theme(spacing[2.5]);
  268. --dp-overlay-col-padding: theme(padding.2);
  269. --dp-time-inc-dec-button-size: theme(size.7);
  270. --dp-menu-padding: theme(padding.2);
  271. --dp-font-size: theme(fontSize.sm);
  272. --dp-preview-font-size: theme(fontSize.xs);
  273. --dp-time-font-size: theme(fontSize.base);
  274. .dp {
  275. &__input_wrap {
  276. display: flex;
  277. }
  278. &__input_icon {
  279. left: unset;
  280. right: theme(space[2.5]);
  281. &:where([dir='rtl'], [dir='rtl'] *) {
  282. left: theme(space[2.5]);
  283. right: unset;
  284. }
  285. &_pad {
  286. padding-inline-start: var(--dp-common-padding);
  287. padding-inline-end: var(--dp-input-icon-padding);
  288. }
  289. }
  290. &__clear_icon {
  291. right: theme(space.6);
  292. &:where([dir='rtl'], [dir='rtl'] *) {
  293. left: theme(space.6);
  294. right: unset;
  295. }
  296. }
  297. &--tp-wrap {
  298. padding: var(--dp-common-padding);
  299. max-width: none;
  300. }
  301. &__inner_nav:hover,
  302. &__month_year_select:hover,
  303. &__year_select:hover,
  304. &__date_hover:hover,
  305. &__inc_dec_button {
  306. background: theme(colors.transparent);
  307. transition: none;
  308. }
  309. &__date_hover.dp__cell_offset:hover {
  310. color: var(--dp-secondary-color);
  311. }
  312. &__menu_inner {
  313. padding-bottom: 0;
  314. }
  315. &__action_row {
  316. padding-top: 0;
  317. margin-top: theme(space[0.5]);
  318. }
  319. &__btn,
  320. &__button,
  321. &__calendar_item,
  322. &__action_button {
  323. transition: none;
  324. border-radius: theme(borderRadius.md);
  325. outline-color: theme(colors.transparent);
  326. &:hover {
  327. outline-width: 1px;
  328. outline-style: solid;
  329. outline-offset: 1px;
  330. }
  331. &:focus {
  332. outline-width: 1px;
  333. outline-style: solid;
  334. outline-offset: 1px;
  335. }
  336. }
  337. &__calendar_row {
  338. gap: theme(gap[1.5]);
  339. }
  340. &__month_year_wrap {
  341. gap: theme(gap.2);
  342. }
  343. &__time_col {
  344. gap: theme(gap.3);
  345. }
  346. &__today {
  347. border: none;
  348. color: theme(colors.blue.800);
  349. &.dp__range_start,
  350. &.dp__range_end,
  351. &.dp__active_date {
  352. color: theme(colors.white);
  353. }
  354. }
  355. &__action_buttons {
  356. margin-inline-start: 0;
  357. flex-grow: 1;
  358. }
  359. &__action_button {
  360. margin-inline-start: 0;
  361. transition: none;
  362. flex-grow: 1;
  363. display: inline-flex;
  364. justify-content: center;
  365. border-radius: theme(borderRadius.md);
  366. }
  367. &__action_cancel {
  368. border: 0;
  369. }
  370. &--arrow-btn-nav .dp__inner_nav {
  371. color: theme(colors.blue.800);
  372. }
  373. /* NB: Fix orientation of the popover arrow in RTL locales. */
  374. &__arrow {
  375. &_top:where([dir='rtl'], [dir='rtl'] *) {
  376. transform: translate(-50%, -50%) rotate(45deg);
  377. }
  378. &_bottom:where([dir='rtl'], [dir='rtl'] *) {
  379. transform: translate(-50%, 50%) rotate(-45deg);
  380. }
  381. }
  382. &__overlay_container + .dp__button,
  383. &__overlay_row + .dp__button {
  384. width: auto;
  385. margin: theme(margin.2);
  386. }
  387. &__overlay_container + .dp__button {
  388. width: calc(var(--dp-menu-min-width) - theme(margin[1.5]) * 2);
  389. }
  390. &__time_display {
  391. transition: none;
  392. padding: theme(padding.2);
  393. }
  394. &__range_start,
  395. &__range_end,
  396. &__range_between {
  397. transition: none;
  398. border: none;
  399. border-radius: theme(borderRadius.md);
  400. }
  401. &__range_between:hover {
  402. background: var(--dp-range-between-dates-background-color);
  403. color: var(--dp-range-between-dates-text-color);
  404. }
  405. &__range_end,
  406. &__range_start,
  407. &__active_date {
  408. &.dp__cell_offset {
  409. color: var(--dp-primary-text-color);
  410. }
  411. }
  412. &__calendar_header {
  413. font-weight: 400;
  414. text-transform: uppercase;
  415. }
  416. }
  417. }
  418. </style>