| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423 |
- <!--
- Date range picker, taken from the following non-maintained project https://github.com/praveenpuglia/vuetify-daterange-picker
- -->
- <template>
- <div class="v-date-range">
- <v-menu
- v-model="menu"
- :close-on-content-click="false"
- offset-y
- v-bind="menuProps"
- >
- <template v-slot:activator="{ on }">
- <v-text-field
- v-on="on"
- class="v-date-range__input-field"
- :value="inputValue"
- readonly
- clearable
- hide-details
- :disabled="disabled"
- @click:clear="reset"
- :label="placeholder"
- append-icon="mdi-calendar"
- v-bind="inputProps"
- ></v-text-field>
- </template>
- <v-card class="v-date-range__menu-content">
- <v-card-text>
- <div
- :data-days="highlightDates.length"
- :class="{
- 'v-date-range__pickers': true,
- 'v-date-range--highlighted': highlightDates.length
- }"
- >
- <v-card-title v-if="$slots.title">
- <slot name="title" v-if="$slots.title"></slot>
- </v-card-title>
- <v-card-text>
- <div class="v-date-range__content">
- <v-list v-if="!noPresets" class="mr-4">
- <v-subheader>{{ presetLabel }}</v-subheader>
- <v-list-item
- v-for="(preset, index) in presets"
- v-model="isPresetActive[index]"
- :key="index"
- @click="selectPreset(index)"
- >
- <v-list-item-content>
- {{ preset.label }}
- </v-list-item-content>
- </v-list-item>
- </v-list>
- <v-date-picker
- class="mr-4 v-date-range__picker--start v-date-range__picker"
- v-model="pickerStart"
- :locale="locale"
- :first-day-of-week="firstDayOfWeek"
- :min="min"
- :max="pickerEnd || max"
- :no-title="noTitle"
- :next-icon="nextIcon"
- :prev-icon="prevIcon"
- :events="highlightDates"
- :event-color="highlightClasses"
- ></v-date-picker>
- <v-date-picker
- class="v-date-range__picker--end v-date-range__picker"
- v-model="pickerEnd"
- :locale="locale"
- :first-day-of-week="firstDayOfWeek"
- :min="pickerStart || min"
- :max="max"
- :no-title="noTitle"
- :next-icon="nextIcon"
- :prev-icon="prevIcon"
- :events="highlightDates"
- :event-color="highlightClasses"
- ></v-date-picker>
- </div>
- </v-card-text>
- </div>
- </v-card-text>
- <v-card-actions>
- <v-spacer></v-spacer>
- <v-btn text @click="reset">{{ mergedActionLabels.reset }}</v-btn>
- <v-btn text @click="menu = false">{{
- mergedActionLabels.cancel
- }}</v-btn>
- <v-btn
- @click="applyRange"
- color="primary"
- :disabled="!bothSelected"
- >{{ mergedActionLabels.apply }}</v-btn
- >
- </v-card-actions>
- </v-card>
- </v-menu>
- </div>
- </template>
- <script lang="ts">
- import { format, parse, differenceInCalendarDays, addDays } from 'date-fns';
- import { formatIsoDate } from '@/services/utils/date'
- import Vue from "vue";
- interface ActionLabels { apply: string, cancel: string, reset: string }
- const ISO_FORMAT: string = 'yyyy-MM-dd';
- const DEFAULT_DATE: string = format(new Date(), ISO_FORMAT);
- const DEFAULT_ACTION_LABELS: ActionLabels = {
- apply: 'Apply',
- cancel: 'Cancel',
- reset: 'Reset'
- };
- export default Vue.extend({
- props: {
- // Take start and end as the input. Passable via v-model.
- value: {
- type: Object as () => DateRange,
- default: () => {
- return { start: '', end: '' };
- }
- },
- disabled: {
- type: Boolean as () => boolean,
- default: false
- },
- presets: {
- type: Array as () => Array<DateRangePreset>,
- default: () => {
- return [];
- }
- },
- noPresets: {
- type: Boolean as () => boolean,
- default: false
- },
- // Denotes the Placeholder string for start date.
- startLabel: {
- type: String as () => string,
- default: 'Start Date'
- },
- // Denotes the Placeholder string for start date.
- endLabel: {
- type: String as () => string,
- default: 'End Date'
- },
- // The string that gets placed between `startLabel` and `endLabel`
- separatorLabel: {
- type: String as () => string,
- default: 'To'
- },
- presetLabel: {
- type: String as () => string,
- default: 'Presets'
- },
- actionLabels: {
- type: Object as () => ActionLabels,
- default: () => {
- return DEFAULT_ACTION_LABELS;
- }
- },
- /**
- * Following values are all passable to vuetify's own datepicker
- * component.
- */
- // Min selectable date.
- min: {
- type: String as () => string,
- default: undefined
- },
- // Max selectable date
- max: {
- type: String as () => string,
- default: undefined
- },
- // Locale
- locale: {
- type: String as () => string,
- default: 'en-us'
- },
- firstDayOfWeek: {
- type: [String, Number] as [() => string, () => number],
- default: 0
- },
- noTitle: {
- type: Boolean as () => boolean,
- default: false
- },
- displayFormat: {
- type: String as () => string
- },
- highlightColor: {
- type: String as () => string,
- default: 'blue lighten-5'
- },
- showReset: {
- type: Boolean as () => boolean,
- default: true
- },
- /**
- * Icons
- */
- nextIcon: {
- type: String,
- default: '$vuetify.icons.next'
- },
- prevIcon: {
- type: String,
- default: '$vuetify.icons.prev'
- },
- inputProps: {
- type: Object,
- default: () => {
- return {};
- }
- },
- menuProps: {
- type: Object,
- default: () => {
- return {};
- }
- }
- },
- data() {
- return {
- menu: false,
- pickerStart: this.value.start as string,
- pickerEnd: this.value.end as string,
- highlightDates: [] as Array<string>,
- highlightClasses: {}
- };
- },
- computed: {
- inputValue(): string {
- if (this.isValueEmpty) {
- return '';
- }
- const start = this.displayFormat
- ? formatIsoDate(this.value.start, this.displayFormat)
- : this.value.start;
- const end = this.displayFormat
- ? formatIsoDate(this.value.end, this.displayFormat)
- : this.value.end;
- return `${start} ${this.separatorLabel} ${end}`;
- },
- placeholder(): string {
- return `${this.startLabel} ${this.separatorLabel} ${this.endLabel}`;
- },
- /**
- * If the value prop doesn't have any start value,
- * its most likely that an empty object was passed.
- */
- isValueEmpty(): boolean {
- return !this.value.start;
- },
- /**
- * If the user has selected both the dates or not
- */
- bothSelected(): boolean {
- return !!this.pickerStart && !!this.pickerEnd;
- },
- isPresetActive(): Array<boolean> {
- return this.presets.map(
- preset =>
- preset.range.start === this.pickerStart &&
- preset.range.end === this.pickerEnd
- );
- },
- mergedActionLabels(): ActionLabels {
- return { ...DEFAULT_ACTION_LABELS, ...this.actionLabels };
- }
- },
- methods: {
- /**
- * Emit the input event with the updated range data.
- * This makes v-model work.
- */
- applyRange() {
- this.menu = false;
- this.emitRange();
- },
- /**
- * Called every time the menu is closed.
- * It also emits an event to tell the parent
- * that the menu has closed.
- *
- * Upon closing the datepicker values are set
- * to the current selected value.
- */
- closeMenu() {
- // Reset the changed values for datepicker models.
- this.pickerStart = this.value.start;
- this.pickerEnd = this.value.end;
- this.highlight();
- this.$emit('menu-closed');
- },
- highlight() {
- if (!this.bothSelected) {
- return;
- }
- const dates = [];
- const classes = {} as Array<string>;
- const start = parse(this.pickerStart, ISO_FORMAT, new Date());
- const end = parse(this.pickerEnd, ISO_FORMAT, new Date());
- const diff = Math.abs(differenceInCalendarDays(start, end));
- // Loop though all the days in range.
- for (let i = 0; i <= diff; i++) {
- const date: string = format(addDays(start, i), ISO_FORMAT);
- dates.push(date);
- const classesArr = [];
- classesArr.push(`v-date-range__in-range`);
- classesArr.push(this.highlightColor);
- i === 0 && classesArr.push(`v-date-range__range-start`);
- i === diff && classesArr.push(`v-date-range__range-end`);
- classes[date as any] = classesArr.join(' ');
- }
- this.highlightDates = dates;
- this.highlightClasses = classes;
- },
- selectPreset(presetIndex: number) {
- this.pickerStart = this.presets[presetIndex].range.start;
- this.pickerEnd = this.presets[presetIndex].range.end;
- },
- reset() {
- // Reset Picker Values
- this.pickerStart = '';
- this.pickerEnd = '';
- this.highlightDates = [];
- this.highlightClasses = {};
- this.emitRange();
- },
- emitRange() {
- this.$emit('input', {
- start: this.pickerStart,
- end: this.pickerEnd
- });
- }
- },
- watch: {
- // Watching to see if the menu is closed.
- menu(isOpen: boolean) {
- if (!isOpen) {
- this.closeMenu();
- } else {
- this.highlight();
- }
- },
- pickerStart: 'highlight',
- pickerEnd: 'highlight'
- }
- })
- </script>
- <style>
- .v-date-range__input-field ::placeholder {
- color: #666666;
- opacity: 1; /* Firefox */
- }
- .v-menu__content {
- top: 144px !important;
- }
- .v-date-range__content > .v-date-picker-table .v-btn {
- border-radius: 0;
- }
- .v-date-range__pickers .v-date-picker-table table {
- width: auto;
- margin: auto;
- border-collapse: collapse;
- }
- .v-date-range__pickers .v-date-picker-table .v-btn {
- position: initial;
- }
- .v-date-range__pickers .v-date-range__content {
- display: flex;
- }
- .v-date-range__pickers .v-date-picker-table__events {
- height: 100%;
- width: 100%;
- top: 0;
- z-index: -1;
- }
- .v-date-range__pickers .v-date-picker-table__events .v-date-range__in-range {
- position: absolute;
- z-index: 0;
- width: 100%;
- height: 100%;
- left: 0;
- top: 0;
- bottom: 0;
- right: 0;
- border-radius: 0;
- }
- .v-date-range__pickers .v-date-picker-table__events .v-date-range__in-range.v-date-range__range-start {
- border-top-left-radius: 50%;
- border-bottom-left-radius: 50%;
- }
- .v-date-range__pickers .v-date-picker-table__events .v-date-range__in-range.v-date-range__range-end {
- border-top-right-radius: 50%;
- border-bottom-right-radius: 50%;
- width: calc(100% - 5px);
- }
- </style>
|