Skip to content

Commit

Permalink
feat(VDatePicker): pick date range (#8891)
Browse files Browse the repository at this point in the history
* feat(vdatepicker): pick date range

fixes #1646

* test(vdatepicker): add unit test

* Update packages/vuetify/src/components/VDatePicker/__tests__/VDatePicker.date.spec.ts

Co-Authored-By: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* refactor(vdatepicker): fix based on review

* refactor(vdatepicker): fix based on review

* docs(vdatepicker): add docs for range prop

* Update packages/docs/src/lang/en/components/DatePickers.json

Co-Authored-By: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* docs(vdatepicker): fix based on review
  • Loading branch information
YipingRuan authored and johnleider committed Sep 24, 2019
1 parent 100b2ed commit 64867ba
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 14 deletions.
1 change: 1 addition & 0 deletions packages/docs/src/data/pages/components/DatePickers.json
Expand Up @@ -61,6 +61,7 @@
"intermediate/date-formatting",
"intermediate/date-formatting-moment-datefns",
"intermediate/date-multiple",
"intermediate/date-range",
"intermediate/date-birthday",
"intermediate/date-events",
"intermediate/month-dialog-and-menu"
Expand Down
@@ -0,0 +1,24 @@
<template>
<v-row>
<v-col cols="12" sm="6">
<v-date-picker v-model="dates" range></v-date-picker>
</v-col>
<v-col cols="12" sm="6">
<v-text-field v-model="dateRangeText" label="Date range" prepend-icon="event" readonly></v-text-field>
model: {{ dates }}
</v-col>
</v-row>
</template>

<script>
export default {
data: () => ({
dates: ['2019-09-10', '2019-09-20'],
}),
computed: {
dateRangeText () {
return this.dates.join(' ~ ')
},
},
}
</script>
5 changes: 5 additions & 0 deletions packages/docs/src/lang/en/components/DatePickers.json
Expand Up @@ -44,6 +44,10 @@
"desc": "Date picker can now select multiple dates with the `multiple` prop. If using `multiple` then date picker expects its model to be an array.",
"header": "### Date pickers - Mutiple"
},
"date-range": {
"desc": "Date picker can select date range with the `range` prop. When using `range` prop date picker expects its model to be an array of length 2 or empty.",
"header": "### Date pickers - Range"
},
"date-picker-date": {
"desc": "You can watch the `pickerDate` which is the displayed month/year (depending on the picker type and active view) to perform some action when it changes.",
"header": "### Date pickers - react to displayed month/year change"
Expand Down Expand Up @@ -122,6 +126,7 @@
"nextIcon": "Sets the icon for next month/year button",
"pickerDate": "Displayed year/month",
"prevIcon": "Sets the icon for previous month/year button",
"range": "Allow the selection of date range",
"reactive": "Updates the picker model when changing months/years automatically",
"readonly": "Makes the picker readonly (doesnt't allow to select new date)",
"scrollable": "Allows changing displayed month with mouse scroll",
Expand Down
11 changes: 11 additions & 0 deletions packages/kitchen/src/pan/Date pickers.vue
Expand Up @@ -388,6 +388,16 @@
/>
</v-layout>
</core-section>

<core-title>
Select range
</core-title>
<core-section center>
<v-date-picker
v-model="dateRange"
range
/>
</core-section>
</v-layout>
</v-container>
</template>
Expand All @@ -402,6 +412,7 @@
model: '2019-01-16',
modelMM: '2019-01-16',
landscape: false,
dateRange: [],
}),
methods: {
Expand Down
52 changes: 38 additions & 14 deletions packages/vuetify/src/components/VDatePicker/VDatePicker.ts
Expand Up @@ -79,6 +79,7 @@ export default mixins(
type: String,
default: '$vuetify.icons.prev',
},
range: Boolean,
reactive: Boolean,
readonly: Boolean,
scrollable: Boolean,
Expand Down Expand Up @@ -120,21 +121,24 @@ export default mixins(
return this.pickerDate
}

const date = (this.multiple ? (this.value as string[])[(this.value as string[]).length - 1] : this.value) ||
const date = (this.multiple || this.range ? (this.value as string[])[(this.value as string[]).length - 1] : this.value) ||
`${now.getFullYear()}-${now.getMonth() + 1}`
return sanitizeDateString(date as string, this.type === 'date' ? 'month' : 'year')
})(),
}
},

computed: {
isMultiple (): boolean {
return this.multiple || this.range
},
lastValue (): string | null {
return this.multiple ? (this.value as string[])[(this.value as string[]).length - 1] : (this.value as string | null)
return this.isMultiple ? (this.value as string[])[(this.value as string[]).length - 1] : (this.value as string | null)
},
selectedMonths (): string | string[] | undefined {
if (!this.value || !this.value.length || this.type === 'month') {
return this.value
} else if (this.multiple) {
} else if (this.isMultiple) {
return (this.value as string[]).map(val => val.substr(0, 7))
} else {
return (this.value as string).substr(0, 7)
Expand Down Expand Up @@ -173,7 +177,8 @@ export default mixins(
formatters (): Formatters {
return {
year: this.yearFormat || createNativeLocaleFormatter(this.currentLocale, { year: 'numeric', timeZone: 'UTC' }, { length: 4 }),
titleDate: this.titleDateFormat || (this.multiple ? this.defaultTitleMultipleDateFormatter : this.defaultTitleDateFormatter),
titleDate: this.titleDateFormat ||
(this.isMultiple ? this.defaultTitleMultipleDateFormatter : this.defaultTitleDateFormatter),
}
},
defaultTitleMultipleDateFormatter (): DatePickerMultipleFormatter {
Expand Down Expand Up @@ -230,20 +235,20 @@ export default mixins(
this.checkMultipleProp()
this.setInputDate()

if (!this.multiple && this.value && !this.pickerDate) {
if (!this.isMultiple && this.value && !this.pickerDate) {
this.tableDate = sanitizeDateString(this.inputDate, this.type === 'month' ? 'year' : 'month')
} else if (this.multiple && (this.value as string[]).length && !(oldValue as string[]).length && !this.pickerDate) {
} else if (this.isMultiple && (this.value as string[]).length && !(oldValue as string[]).length && !this.pickerDate) {
this.tableDate = sanitizeDateString(this.inputDate, this.type === 'month' ? 'year' : 'month')
}
},
type (type: DatePickerType) {
this.activePicker = type.toUpperCase()

if (this.value && this.value.length) {
const output = (this.multiple ? (this.value as string[]) : [this.value as string])
const output = (this.isMultiple ? (this.value as string[]) : [this.value as string])
.map((val: string) => sanitizeDateString(val, type))
.filter(this.isDateAllowed)
this.$emit('input', this.multiple ? output : output[0])
this.$emit('input', this.isMultiple ? output : output[0])
}
},
},
Expand All @@ -259,6 +264,13 @@ export default mixins(

methods: {
emitInput (newInput: string) {
if (this.range && this.value) {
this.value.length === 2
? this.$emit('input', [newInput])
: this.$emit('input', [...this.value, newInput])
return
}

const output = this.multiple
? (
(this.value as string[]).indexOf(newInput) === -1
Expand All @@ -273,9 +285,9 @@ export default mixins(
checkMultipleProp () {
if (this.value == null) return
const valueType = this.value.constructor.name
const expected = this.multiple ? 'Array' : 'String'
const expected = this.isMultiple ? 'Array' : 'String'
if (valueType !== expected) {
consoleWarn(`Value must be ${this.multiple ? 'an' : 'a'} ${expected}, got ${valueType}`, this)
consoleWarn(`Value must be ${this.isMultiple ? 'an' : 'a'} ${expected}, got ${valueType}`, this)
}
},
isDateAllowed (value: string) {
Expand All @@ -289,7 +301,7 @@ export default mixins(
this.tableDate = `${value}-${pad((this.tableMonth || 0) + 1)}`
}
this.activePicker = 'MONTH'
if (this.reactive && !this.readonly && !this.multiple && this.isDateAllowed(this.inputDate)) {
if (this.reactive && !this.readonly && !this.isMultiple && this.isDateAllowed(this.inputDate)) {
this.$emit('input', this.inputDate)
}
},
Expand All @@ -303,7 +315,7 @@ export default mixins(

this.tableDate = value
this.activePicker = 'DATE'
if (this.reactive && !this.readonly && !this.multiple && this.isDateAllowed(this.inputDate)) {
if (this.reactive && !this.readonly && !this.isMultiple && this.isDateAllowed(this.inputDate)) {
this.$emit('input', this.inputDate)
}
} else {
Expand All @@ -325,7 +337,7 @@ export default mixins(
selectingYear: this.activePicker === 'YEAR',
year: this.formatters.year(this.value ? `${this.inputYear}` : this.tableDate),
yearIcon: this.yearIcon,
value: this.multiple ? (this.value as string[])[0] : this.value,
value: this.isMultiple ? (this.value as string[])[0] : this.value,
},
slot: 'title',
on: {
Expand Down Expand Up @@ -356,6 +368,18 @@ export default mixins(
})
},
genDateTable () {
let proxyValue = this.value

if (this.range && this.value && this.value.length === 2) {
proxyValue = []
const [rangeFrom, rangeTo] = [this.value[0], this.value[1]].map(x => new Date(`${x}T00:00:00+00:00`)).sort((a, b) => a > b ? 1 : -1)
const diffDays = Math.ceil((rangeTo.getTime() - rangeFrom.getTime()) / (1000 * 60 * 60 * 24))
for (let i = 0; i <= diffDays; i++) {
const current = new Date(+rangeFrom + i * 864e5)
proxyValue.push(current.toISOString().substring(0, 10))
}
}

return this.$createElement(VDatePickerDateTable, {
props: {
allowedDates: this.allowedDates,
Expand All @@ -375,7 +399,7 @@ export default mixins(
scrollable: this.scrollable,
showWeek: this.showWeek,
tableDate: `${pad(this.tableYear, 4)}-${pad(this.tableMonth + 1)}`,
value: this.value,
value: proxyValue,
weekdayFormat: this.weekdayFormat,
},
ref: 'table',
Expand Down
Expand Up @@ -633,4 +633,24 @@ describe('VDatePicker.ts', () => { // eslint-disable-line max-statements
wrapper.findAll('.v-date-picker-table--date tbody tr+tr td:first-child button').at(0).trigger('dblclick')
expect(dblclick).toHaveBeenCalledWith('2013-05-05')
})

it('should handle date range select', async () => {
const cb = jest.fn()
const wrapper = mountFunction({
propsData: {
range: true,
value: ['2019-01-01', '2019-01-02'],
},
})

wrapper.vm.$on('input', cb)
wrapper.findAll('.v-date-picker-table--date tbody tr+tr td:first-child button').at(0).trigger('click')
expect(cb.mock.calls[0][0]).toEqual(
expect.arrayContaining(['2019-01-06'])
)

wrapper.findAll('.v-date-picker-table--date tbody tr+tr td button').at(2).trigger('click')
expect(cb.mock.calls[0][0][0]).toBe('2019-01-06')
expect(cb.mock.calls[1][0][0]).toBe('2019-01-08')
})
})

0 comments on commit 64867ba

Please sign in to comment.