client/components/date-range-picker/index.vue (287 lines of code) (raw):

<script> // Copyright (c) 2017-2024 Uber Technologies Inc. // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import moment from 'moment'; import DatePicker from 'vue2-datepicker'; import { getDateString, getMaxEndDate, getRange, getRangeDisplayText, getShortcuts, getTimePanelLabel, isDateValid, isDayDisabled, } from './helpers'; export default { props: ['dateRange', 'maxDays', 'minStartDate'], data() { const range = getRange(this.dateRange); return { interval: undefined, range, startTimeString: getDateString(range[0]), endTimeString: getDateString(range[1]), now: new Date(), open: false, showTimePanel: false, }; }, mounted() { this.interval = setInterval(() => { this.now = new Date(); }, 60 * 1000); }, beforeDestroy() { clearInterval(this.interval); }, computed: { endTime() { return moment(this.endTimeString); }, endTimeInvalid() { return !isDateValid(this.endTime, this.minStartDate, this.maxEndDate); }, isDayDisabled() { return isDayDisabled(this.minStartDate); }, maxEndDate() { return getMaxEndDate(this.now); }, rangeDisplayText() { return getRangeDisplayText(this.dateRange); }, shortcuts() { return getShortcuts(this.maxDays, this.minStartDate); }, startTime() { return moment(this.startTimeString); }, startTimeInvalid() { return !isDateValid(this.startTime, this.minStartDate, this.maxEndDate); }, startOrEndTimeInvalid() { return this.startTimeInvalid || this.endTimeInvalid; }, timePanelLabel() { return getTimePanelLabel(this.showTimePanel); }, }, methods: { onShortcutClick(shortcut) { this.$emit('change', shortcut.value); this.open = false; }, onDateRangeChange(range) { const [startTime, endTime] = range; if (!startTime || !endTime) { return; } this.$emit('change', { startTime, endTime }); }, onCustomRangeSubmit() { let { startTime, endTime } = this; if (this.startOrEndTimeInvalid) { return; } if (endTime.isBefore(startTime)) { startTime = this.endTime; endTime = this.startTime; } this.$emit('change', { startTime, endTime, }); this.open = false; }, onClickTimePanelLabel() { this.showTimePanel = !this.showTimePanel; }, }, components: { DatePicker, }, watch: { dateRange() { this.range = getRange(this.dateRange); this.startTimeString = getDateString(this.range[0]); this.endTimeString = getDateString(this.range[1]); }, }, }; </script> <template> <div class="date-range-picker"> <date-picker range type="datetime" v-model="range" :clearable="false" :disabled-date="isDayDisabled" :showTimePanel="showTimePanel" :open.sync="open" @change="onDateRangeChange" > <template v-slot:input> <input placeholder="Date Range" readonly type="text" :value="rangeDisplayText" /> </template> <template v-slot:sidebar> <div class="sidebar-column sidebar-column-shortcuts"> <h5>Quick ranges</h5> <button class="mx-btn mx-btn-text mx-btn-shortcut" type="button" v-for="shortcut in shortcuts" :key="shortcut.value" @click="onShortcutClick(shortcut)" > {{ shortcut.text }} </button> </div> <div class="sidebar-column sidebar-column-custom-range"> <form @submit.prevent="onCustomRangeSubmit"> <h5>Custom range</h5> <div> <label for="custom-range-from">From:</label> <input id="custom-range-from" maxlength="19" v-model="startTimeString" :class="{ invalid: startTimeInvalid }" /> </div> <div> <label for="custom-range-to">To:</label> <input id="custom-range-to" maxlength="19" v-model="endTimeString" :class="{ invalid: endTimeInvalid }" /> </div> <div> <button class="sidebar-button" type="submit" :disabled="startOrEndTimeInvalid" > Apply </button> </div> </form> </div> </template> <template v-slot:footer> <button class="mx-btn mx-btn-text" @click="onClickTimePanelLabel"> {{ timePanelLabel }} </button> </template> </date-picker> </div> </template> <style lang="stylus"> sidebarColumnCustomRangeWidth = 181px; sidebarColumnShortcutsWidth = 130px; sidebarColumnPadding = 12px; sidebarWidth = sidebarColumnShortcutsWidth + sidebarColumnCustomRangeWidth; .mx-datepicker-popup { td { text-align: center; } .mx-datepicker-sidebar { padding: 6px 0; width: sidebarWidth; &+ .mx-datepicker-content { margin-left: sidebarWidth; } } .sidebar-column { float: left; height: 100%; padding: 0 sidebarColumnPadding; &+ .sidebar-column { border-left: 1px solid #e8e8e8; } label { display: block; font-weight: 500; } button { padding: 0; } input { margin: 0 0 8px 0; padding: 8px 10px; width: 156px; &.invalid { border: 1px solid #D44333; &:focus { outline: #D44333 auto 3px; } } } h5 { text-transform: none; } } .sidebar-column-shortcuts { width: sidebarColumnShortcutsWidth; } .sidebar-column-custom-range { width: sidebarColumnCustomRangeWidth; } button.sidebar-button { background-color: #11939a; border: none; color: #fff; cursor: pointer; display: inline-block; float: right; font-size: 12px; font-weight: bold; padding: 8px 12px; &:disabled { background-color: #75F7FE; cursor: not-allowed; } } } .date-range-picker { input { border: 1px solid #e5e5e4; border-radius: 0; box-shadow: none; cursor: pointer; height: auto; margin: 0; padding: 8px 30px 8px 10px; width: 325px; } .mx-datepicker-range { width: auto; } } </style>