DateRangePicker.vue 11 KB

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