DateRangePicker.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. <!--
  2. Date range picker, taken from the following non-maintained project https://github.com/praveenpuglia/vuetify-daterange-picker
  3. -->
  4. <template>
  5. <div class="v-date-range">
  6. <v-menu
  7. v-model="menu"
  8. :close-on-content-click="false"
  9. offset-y
  10. v-bind="menuProps"
  11. >
  12. <template v-slot:activator="{ on }">
  13. <v-text-field
  14. v-on="on"
  15. class="v-date-range__input-field"
  16. :value="inputValue"
  17. readonly
  18. clearable
  19. hide-details
  20. :disabled="disabled"
  21. @click:clear="reset"
  22. :label="placeholder"
  23. append-icon="mdi-calendar"
  24. v-bind="inputProps"
  25. ></v-text-field>
  26. </template>
  27. <v-card class="v-date-range__menu-content">
  28. <v-card-text>
  29. <div
  30. :data-days="highlightDates.length"
  31. :class="{
  32. 'v-date-range__pickers': true,
  33. 'v-date-range--highlighted': highlightDates.length
  34. }"
  35. >
  36. <v-card-title v-if="$slots.title">
  37. <slot name="title" v-if="$slots.title"></slot>
  38. </v-card-title>
  39. <v-card-text>
  40. <div class="v-date-range__content">
  41. <v-list v-if="!noPresets" class="mr-4">
  42. <v-subheader>{{ presetLabel }}</v-subheader>
  43. <v-list-item
  44. v-for="(preset, index) in presets"
  45. v-model="isPresetActive[index]"
  46. :key="index"
  47. @click="selectPreset(index)"
  48. >
  49. <v-list-item-content>
  50. {{ preset.label }}
  51. </v-list-item-content>
  52. </v-list-item>
  53. </v-list>
  54. <v-date-picker
  55. class="mr-4 v-date-range__picker--start v-date-range__picker"
  56. v-model="pickerStart"
  57. :locale="locale"
  58. :first-day-of-week="firstDayOfWeek"
  59. :min="min"
  60. :max="pickerEnd || max"
  61. :no-title="noTitle"
  62. :next-icon="nextIcon"
  63. :prev-icon="prevIcon"
  64. :events="highlightDates"
  65. :event-color="highlightClasses"
  66. ></v-date-picker>
  67. <v-date-picker
  68. class="v-date-range__picker--end v-date-range__picker"
  69. v-model="pickerEnd"
  70. :locale="locale"
  71. :first-day-of-week="firstDayOfWeek"
  72. :min="pickerStart || min"
  73. :max="max"
  74. :no-title="noTitle"
  75. :next-icon="nextIcon"
  76. :prev-icon="prevIcon"
  77. :events="highlightDates"
  78. :event-color="highlightClasses"
  79. ></v-date-picker>
  80. </div>
  81. </v-card-text>
  82. </div>
  83. </v-card-text>
  84. <v-card-actions>
  85. <v-spacer></v-spacer>
  86. <v-btn text @click="reset">{{ mergedActionLabels.reset }}</v-btn>
  87. <v-btn text @click="menu = false">{{
  88. mergedActionLabels.cancel
  89. }}</v-btn>
  90. <v-btn
  91. @click="applyRange"
  92. color="primary"
  93. :disabled="!bothSelected"
  94. >{{ mergedActionLabels.apply }}</v-btn
  95. >
  96. </v-card-actions>
  97. </v-card>
  98. </v-menu>
  99. </div>
  100. </template>
  101. <script lang="ts">
  102. import { format, parse, differenceInCalendarDays, addDays } from 'date-fns';
  103. import Vue from "vue";
  104. import DatesUtils from "~/services/utils/dateUtils";
  105. interface ActionLabels { apply: string, cancel: string, reset: string }
  106. const ISO_FORMAT: string = 'yyyy-MM-dd';
  107. const DEFAULT_DATE: string = format(new Date(), ISO_FORMAT);
  108. const DEFAULT_ACTION_LABELS: ActionLabels = {
  109. apply: 'Apply',
  110. cancel: 'Cancel',
  111. reset: 'Reset'
  112. };
  113. export default Vue.extend({
  114. props: {
  115. // Take start and end as the input. Passable via v-model.
  116. value: {
  117. type: Object as () => DateRange,
  118. default: () => {
  119. return { start: '', end: '' };
  120. }
  121. },
  122. disabled: {
  123. type: Boolean as () => boolean,
  124. default: false
  125. },
  126. presets: {
  127. type: Array as () => Array<DateRangePreset>,
  128. default: () => {
  129. return [];
  130. }
  131. },
  132. noPresets: {
  133. type: Boolean as () => boolean,
  134. default: false
  135. },
  136. label: {
  137. type: String as () => string | null,
  138. default: null
  139. },
  140. // Denotes the Placeholder string for start date.
  141. startLabel: {
  142. type: String as () => string,
  143. default: 'Start Date'
  144. },
  145. // Denotes the Placeholder string for start date.
  146. endLabel: {
  147. type: String as () => string,
  148. default: 'End Date'
  149. },
  150. // The string that gets placed between `startLabel` and `endLabel`
  151. separatorLabel: {
  152. type: String as () => string,
  153. default: 'To'
  154. },
  155. presetLabel: {
  156. type: String as () => string,
  157. default: 'Presets'
  158. },
  159. actionLabels: {
  160. type: Object as () => ActionLabels,
  161. default: () => {
  162. return DEFAULT_ACTION_LABELS;
  163. }
  164. },
  165. /**
  166. * Following values are all passable to vuetify's own datepicker
  167. * component.
  168. */
  169. // Min selectable date.
  170. min: {
  171. type: String as () => string,
  172. default: undefined
  173. },
  174. // Max selectable date
  175. max: {
  176. type: String as () => string,
  177. default: undefined
  178. },
  179. // Locale
  180. locale: {
  181. type: String as () => string,
  182. default: 'en-us'
  183. },
  184. firstDayOfWeek: {
  185. type: [String, Number] as [() => string, () => number],
  186. default: 0
  187. },
  188. noTitle: {
  189. type: Boolean as () => boolean,
  190. default: false
  191. },
  192. displayFormat: {
  193. type: String as () => string
  194. },
  195. highlightColor: {
  196. type: String as () => string,
  197. default: 'blue lighten-5'
  198. },
  199. showReset: {
  200. type: Boolean as () => boolean,
  201. default: true
  202. },
  203. /**
  204. * Icons
  205. */
  206. nextIcon: {
  207. type: String,
  208. default: '$vuetify.icons.next'
  209. },
  210. prevIcon: {
  211. type: String,
  212. default: '$vuetify.icons.prev'
  213. },
  214. inputProps: {
  215. type: Object,
  216. default: () => {
  217. return {};
  218. }
  219. },
  220. menuProps: {
  221. type: Object,
  222. default: () => {
  223. return {};
  224. }
  225. }
  226. },
  227. data() {
  228. return {
  229. menu: false,
  230. pickerStart: this.value.start as string,
  231. pickerEnd: this.value.end as string,
  232. highlightDates: [] as Array<string>,
  233. highlightClasses: {},
  234. dateUtils: new DatesUtils(this.$dateFns, this.$t, this.$i18n)
  235. };
  236. },
  237. computed: {
  238. inputValue(): string {
  239. if (this.isValueEmpty) {
  240. return '';
  241. }
  242. const start = this.displayFormat
  243. ? this.dateUtils.formatIsoDate(this.value.start, this.displayFormat)
  244. : this.value.start;
  245. const end = this.displayFormat
  246. ? this.dateUtils.formatIsoDate(this.value.end, this.displayFormat)
  247. : this.value.end;
  248. return `${start} ${this.separatorLabel} ${end}`;
  249. },
  250. placeholder(): string {
  251. return this.label !== null ? this.label : `${this.startLabel} ${this.separatorLabel} ${this.endLabel}`;
  252. },
  253. /**
  254. * If the value prop doesn't have any start value,
  255. * its most likely that an empty object was passed.
  256. */
  257. isValueEmpty(): boolean {
  258. return !this.value.start;
  259. },
  260. /**
  261. * If the user has selected both the dates or not
  262. */
  263. bothSelected(): boolean {
  264. return !!this.pickerStart && !!this.pickerEnd;
  265. },
  266. isPresetActive(): Array<boolean> {
  267. return this.presets.map(
  268. preset =>
  269. preset.range.start === this.pickerStart &&
  270. preset.range.end === this.pickerEnd
  271. );
  272. },
  273. mergedActionLabels(): ActionLabels {
  274. return { ...DEFAULT_ACTION_LABELS, ...this.actionLabels };
  275. }
  276. },
  277. methods: {
  278. /**
  279. * Emit the input event with the updated range data.
  280. * This makes v-model work.
  281. */
  282. applyRange() {
  283. this.menu = false;
  284. this.emitRange();
  285. },
  286. /**
  287. * Called every time the menu is closed.
  288. * It also emits an event to tell the parent
  289. * that the menu has closed.
  290. *
  291. * Upon closing the datepicker values are set
  292. * to the current selected value.
  293. */
  294. closeMenu() {
  295. // Reset the changed values for datepicker models.
  296. this.pickerStart = this.value.start;
  297. this.pickerEnd = this.value.end;
  298. this.highlight();
  299. this.$emit('menu-closed');
  300. },
  301. highlight() {
  302. if (!this.bothSelected) {
  303. return;
  304. }
  305. const dates = [];
  306. const classes = {} as Array<string>;
  307. const start = parse(this.pickerStart, ISO_FORMAT, new Date());
  308. const end = parse(this.pickerEnd, ISO_FORMAT, new Date());
  309. const diff = Math.abs(differenceInCalendarDays(start, end));
  310. // Loop though all the days in range.
  311. for (let i = 0; i <= diff; i++) {
  312. const date: string = format(addDays(start, i), ISO_FORMAT);
  313. dates.push(date);
  314. const classesArr = [];
  315. classesArr.push(`v-date-range__in-range`);
  316. classesArr.push(this.highlightColor);
  317. i === 0 && classesArr.push(`v-date-range__range-start`);
  318. i === diff && classesArr.push(`v-date-range__range-end`);
  319. classes[date as any] = classesArr.join(' ');
  320. }
  321. this.highlightDates = dates;
  322. this.highlightClasses = classes;
  323. },
  324. selectPreset(presetIndex: number) {
  325. this.pickerStart = this.presets[presetIndex].range.start;
  326. this.pickerEnd = this.presets[presetIndex].range.end;
  327. },
  328. reset() {
  329. // Reset Picker Values
  330. this.pickerStart = '';
  331. this.pickerEnd = '';
  332. this.highlightDates = [];
  333. this.highlightClasses = {};
  334. this.emitRange();
  335. },
  336. emitRange() {
  337. this.$emit('input', {
  338. start: this.pickerStart,
  339. end: this.pickerEnd
  340. });
  341. }
  342. },
  343. watch: {
  344. // Watching to see if the menu is closed.
  345. menu(isOpen: boolean) {
  346. if (!isOpen) {
  347. this.closeMenu();
  348. } else {
  349. this.highlight();
  350. }
  351. },
  352. pickerStart: 'highlight',
  353. pickerEnd: 'highlight'
  354. }
  355. })
  356. </script>
  357. <style>
  358. .v-date-range__input-field ::placeholder {
  359. color: #666666;
  360. opacity: 1; /* Firefox */
  361. }
  362. .v-menu__content {
  363. top: 144px !important;
  364. }
  365. .v-date-range__content > .v-date-picker-table .v-btn {
  366. border-radius: 0;
  367. }
  368. .v-date-range__pickers .v-date-picker-table table {
  369. width: auto;
  370. margin: auto;
  371. border-collapse: collapse;
  372. }
  373. .v-date-range__pickers .v-date-picker-table .v-btn {
  374. position: initial;
  375. }
  376. .v-date-range__pickers .v-date-range__content {
  377. display: flex;
  378. }
  379. .v-date-range__pickers .v-date-picker-table__events {
  380. height: 100%;
  381. width: 100%;
  382. top: 0;
  383. z-index: -1;
  384. }
  385. .v-date-range__pickers .v-date-picker-table__events .v-date-range__in-range {
  386. position: absolute;
  387. z-index: 0;
  388. width: 100%;
  389. height: 100%;
  390. left: 0;
  391. top: 0;
  392. bottom: 0;
  393. right: 0;
  394. border-radius: 0;
  395. }
  396. .v-date-range__pickers .v-date-picker-table__events .v-date-range__in-range.v-date-range__range-start {
  397. border-top-left-radius: 50%;
  398. border-bottom-left-radius: 50%;
  399. }
  400. .v-date-range__pickers .v-date-picker-table__events .v-date-range__in-range.v-date-range__range-end {
  401. border-top-right-radius: 50%;
  402. border-bottom-right-radius: 50%;
  403. width: calc(100% - 5px);
  404. }
  405. </style>