|
|
@@ -0,0 +1,428 @@
|
|
|
+<!--
|
|
|
+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 Vue from "vue";
|
|
|
+import DatesUtils from "~/services/utils/dateUtils";
|
|
|
+
|
|
|
+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
|
|
|
+ },
|
|
|
+ label: {
|
|
|
+ type: String as () => string | null,
|
|
|
+ default: null
|
|
|
+ },
|
|
|
+ // 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: {},
|
|
|
+ dateUtils: new DatesUtils(this.$dateFns, this.$t, this.$i18n)
|
|
|
+ };
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ inputValue(): string {
|
|
|
+ if (this.isValueEmpty) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ const start = this.displayFormat
|
|
|
+ ? this.dateUtils.formatIsoDate(this.value.start, this.displayFormat)
|
|
|
+ : this.value.start;
|
|
|
+ const end = this.displayFormat
|
|
|
+ ? this.dateUtils.formatIsoDate(this.value.end, this.displayFormat)
|
|
|
+ : this.value.end;
|
|
|
+ return `${start} ${this.separatorLabel} ${end}`;
|
|
|
+ },
|
|
|
+ placeholder(): string {
|
|
|
+ return this.label !== null ? this.label : `${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>
|
|
|
+
|
|
|
+
|
|
|
+
|