bootstrap-vue
Version:
With more than 85 components, over 45 available plugins, several directives, and 1000+ icons, BootstrapVue provides one of the most comprehensive implementations of the Bootstrap v4 component and grid system available for Vue.js v2.6, complete with extens
343 lines (327 loc) • 9.48 kB
JavaScript
import { extend } from '../../vue'
import { NAME_FORM_DATEPICKER } from '../../constants/components'
import { EVENT_NAME_CONTEXT, EVENT_NAME_HIDDEN, EVENT_NAME_SHOWN } from '../../constants/events'
import { PROP_TYPE_BOOLEAN, PROP_TYPE_DATE_STRING, PROP_TYPE_STRING } from '../../constants/props'
import { SLOT_NAME_BUTTON_CONTENT } from '../../constants/slots'
import { createDate, constrainDate, formatYMD, parseYMD } from '../../utils/date'
import { attemptBlur, attemptFocus } from '../../utils/dom'
import { isUndefinedOrNull } from '../../utils/inspect'
import { makeModelMixin } from '../../utils/model'
import { omit, pick, sortKeys } from '../../utils/object'
import { makeProp, makePropsConfigurable, pluckProps } from '../../utils/props'
import { idMixin, props as idProps } from '../../mixins/id'
import { BIconCalendar, BIconCalendarFill } from '../../icons/icons'
import { BButton } from '../button/button'
import { BCalendar, props as BCalendarProps } from '../calendar/calendar'
import {
BVFormBtnLabelControl,
props as BVFormBtnLabelControlProps
} from '../form-btn-label-control/bv-form-btn-label-control'
// --- Constants ---
const {
mixin: modelMixin,
props: modelProps,
prop: MODEL_PROP_NAME,
event: MODEL_EVENT_NAME
} = makeModelMixin('value', { type: PROP_TYPE_DATE_STRING })
// --- Props ---
const calendarProps = omit(BCalendarProps, [
'block',
'hidden',
'id',
'noKeyNav',
'roleDescription',
'value',
'width'
])
const formBtnLabelControlProps = omit(BVFormBtnLabelControlProps, [
'formattedValue',
'id',
'lang',
'rtl',
'value'
])
export const props = makePropsConfigurable(
sortKeys({
...idProps,
...modelProps,
...calendarProps,
...formBtnLabelControlProps,
// Width of the calendar dropdown
calendarWidth: makeProp(PROP_TYPE_STRING, '270px'),
closeButton: makeProp(PROP_TYPE_BOOLEAN, false),
closeButtonVariant: makeProp(PROP_TYPE_STRING, 'outline-secondary'),
// Dark mode
dark: makeProp(PROP_TYPE_BOOLEAN, false),
labelCloseButton: makeProp(PROP_TYPE_STRING, 'Close'),
labelResetButton: makeProp(PROP_TYPE_STRING, 'Reset'),
labelTodayButton: makeProp(PROP_TYPE_STRING, 'Select today'),
noCloseOnSelect: makeProp(PROP_TYPE_BOOLEAN, false),
resetButton: makeProp(PROP_TYPE_BOOLEAN, false),
resetButtonVariant: makeProp(PROP_TYPE_STRING, 'outline-danger'),
resetValue: makeProp(PROP_TYPE_DATE_STRING),
todayButton: makeProp(PROP_TYPE_BOOLEAN, false),
todayButtonVariant: makeProp(PROP_TYPE_STRING, 'outline-primary')
}),
NAME_FORM_DATEPICKER
)
// --- Main component ---
// @vue/component
export const BFormDatepicker = /*#__PURE__*/ extend({
name: NAME_FORM_DATEPICKER,
mixins: [idMixin, modelMixin],
props,
data() {
return {
// We always use `YYYY-MM-DD` value internally
localYMD: formatYMD(this[MODEL_PROP_NAME]) || '',
// If the popup is open
isVisible: false,
// Context data from BCalendar
localLocale: null,
isRTL: false,
formattedValue: '',
activeYMD: ''
}
},
computed: {
calendarYM() {
// Returns the calendar year/month
// Returns the `YYYY-MM` portion of the active calendar date
return this.activeYMD.slice(0, -3)
},
computedLang() {
return (this.localLocale || '').replace(/-u-.*$/i, '') || null
},
computedResetValue() {
return formatYMD(constrainDate(this.resetValue)) || ''
}
},
watch: {
[MODEL_PROP_NAME](newValue) {
this.localYMD = formatYMD(newValue) || ''
},
localYMD(newValue) {
// We only update the v-model when the datepicker is open
if (this.isVisible) {
this.$emit(MODEL_EVENT_NAME, this.valueAsDate ? parseYMD(newValue) || null : newValue || '')
}
},
calendarYM(newValue, oldValue) {
// Displayed calendar month has changed
// So possibly the calendar height has changed...
// We need to update popper computed position
if (newValue !== oldValue && oldValue) {
try {
this.$refs.control.updatePopper()
} catch {}
}
}
},
methods: {
// Public methods
focus() {
if (!this.disabled) {
attemptFocus(this.$refs.control)
}
},
blur() {
if (!this.disabled) {
attemptBlur(this.$refs.control)
}
},
// Private methods
setAndClose(ymd) {
this.localYMD = ymd
// Close calendar popup, unless `noCloseOnSelect`
if (!this.noCloseOnSelect) {
this.$nextTick(() => {
this.$refs.control.hide(true)
})
}
},
onSelected(ymd) {
this.$nextTick(() => {
this.setAndClose(ymd)
})
},
onInput(ymd) {
if (this.localYMD !== ymd) {
this.localYMD = ymd
}
},
onContext(ctx) {
const { activeYMD, isRTL, locale, selectedYMD, selectedFormatted } = ctx
this.isRTL = isRTL
this.localLocale = locale
this.formattedValue = selectedFormatted
this.localYMD = selectedYMD
this.activeYMD = activeYMD
// Re-emit the context event
this.$emit(EVENT_NAME_CONTEXT, ctx)
},
onTodayButton() {
// Set to today (or min/max if today is out of range)
this.setAndClose(formatYMD(constrainDate(createDate(), this.min, this.max)))
},
onResetButton() {
this.setAndClose(this.computedResetValue)
},
onCloseButton() {
this.$refs.control.hide(true)
},
// Menu handlers
onShow() {
this.isVisible = true
},
onShown() {
this.$nextTick(() => {
attemptFocus(this.$refs.calendar)
this.$emit(EVENT_NAME_SHOWN)
})
},
onHidden() {
this.isVisible = false
this.$emit(EVENT_NAME_HIDDEN)
},
// Render helpers
defaultButtonFn({ isHovered, hasFocus }) {
return this.$createElement(isHovered || hasFocus ? BIconCalendarFill : BIconCalendar, {
attrs: { 'aria-hidden': 'true' }
})
}
},
render(h) {
const { localYMD, disabled, readonly, dark, $props, $scopedSlots } = this
const placeholder = isUndefinedOrNull(this.placeholder)
? this.labelNoDateSelected
: this.placeholder
// Optional footer buttons
let $footer = []
if (this.todayButton) {
const label = this.labelTodayButton
$footer.push(
h(
BButton,
{
props: {
disabled: disabled || readonly,
size: 'sm',
variant: this.todayButtonVariant
},
attrs: { 'aria-label': label || null },
on: { click: this.onTodayButton }
},
label
)
)
}
if (this.resetButton) {
const label = this.labelResetButton
$footer.push(
h(
BButton,
{
props: {
disabled: disabled || readonly,
size: 'sm',
variant: this.resetButtonVariant
},
attrs: { 'aria-label': label || null },
on: { click: this.onResetButton }
},
label
)
)
}
if (this.closeButton) {
const label = this.labelCloseButton
$footer.push(
h(
BButton,
{
props: {
disabled,
size: 'sm',
variant: this.closeButtonVariant
},
attrs: { 'aria-label': label || null },
on: { click: this.onCloseButton }
},
label
)
)
}
if ($footer.length > 0) {
$footer = [
h(
'div',
{
staticClass: 'b-form-date-controls d-flex flex-wrap',
class: {
'justify-content-between': $footer.length > 1,
'justify-content-end': $footer.length < 2
}
},
$footer
)
]
}
const $calendar = h(
BCalendar,
{
staticClass: 'b-form-date-calendar w-100',
props: {
...pluckProps(calendarProps, $props),
hidden: !this.isVisible,
value: localYMD,
valueAsDate: false,
width: this.calendarWidth
},
on: {
selected: this.onSelected,
input: this.onInput,
context: this.onContext
},
scopedSlots: pick($scopedSlots, [
'nav-prev-decade',
'nav-prev-year',
'nav-prev-month',
'nav-this-month',
'nav-next-month',
'nav-next-year',
'nav-next-decade'
]),
key: 'calendar',
ref: 'calendar'
},
$footer
)
return h(
BVFormBtnLabelControl,
{
staticClass: 'b-form-datepicker',
props: {
...pluckProps(formBtnLabelControlProps, $props),
formattedValue: localYMD ? this.formattedValue : '',
id: this.safeId(),
lang: this.computedLang,
menuClass: [{ 'bg-dark': dark, 'text-light': dark }, this.menuClass],
placeholder,
rtl: this.isRTL,
value: localYMD
},
on: {
show: this.onShow,
shown: this.onShown,
hidden: this.onHidden
},
scopedSlots: {
[SLOT_NAME_BUTTON_CONTENT]: $scopedSlots[SLOT_NAME_BUTTON_CONTENT] || this.defaultButtonFn
},
ref: 'control'
},
[$calendar]
)
}
})