FieldDateTimeInput.vue 13 KB

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