FieldDateTimeInput.vue 13 KB

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