Skip to content

Commit

Permalink
feat(VCalendar): add support for object categories (#12518)
Browse files Browse the repository at this point in the history
Co-authored-by: Nathan <nquinn@medonesystems.com>
  • Loading branch information
nquinn721 and Nathan committed Nov 11, 2020
1 parent 9cd505d commit 806864c
Show file tree
Hide file tree
Showing 10 changed files with 191 additions and 47 deletions.
1 change: 1 addition & 0 deletions packages/api-generator/src/locale/en/v-calendar.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"categoryShowAll": "Set whether the `category` view should show all defined `categories` even if there are no events for a category.",
"categoryForInvalid": "The category to place events in that have invalid categories. A category is invalid when it is not a string. By default events without a category are not displayed until this value is specified.",
"categoryDays": "The number of days to render in the `category` view.",
"categoryText": "If categories is a list of objects, you can use this to determine what property to print out as the category text on the calendar. You can provide a function to do some logic or just define the prop name. It's similar to item-text on v-select",
"dayFormat": "Formats day of the month string that appears in a day to a specified locale",
"end": "The ending date on the calendar (inclusive) in the format of `YYYY-MM-DD`. This may be ignored depending on the `type` of the calendar.",
"eventCategory": "Set property of *event*'s category. Instead of a property a function can be given which takes an event and returns the category.",
Expand Down
25 changes: 13 additions & 12 deletions packages/vuetify/src/components/VCalendar/VCalendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ import VCalendarMonthly from './VCalendarMonthly'
import VCalendarDaily from './VCalendarDaily'
import VCalendarWeekly from './VCalendarWeekly'
import VCalendarCategory from './VCalendarCategory'
import { CalendarTimestamp, CalendarFormatter } from 'vuetify/types'
import { CalendarTimestamp, CalendarFormatter, CalendarCategory } from 'vuetify/types'
import { getParsedCategories } from './util/parser'

// Types
interface VCalendarRenderProps {
Expand All @@ -43,7 +44,7 @@ interface VCalendarRenderProps {
component: string | Component
maxDays: number
weekdays: number[]
categories: string[]
categories: CalendarCategory[]
}

/* @vue/component */
Expand Down Expand Up @@ -170,12 +171,8 @@ export default CalendarWithEvents.extend({
timeZone: 'UTC', month: 'short',
})
},
parsedCategories (): string[] {
return typeof this.categories === 'string' && this.categories
? this.categories.split(/\s*,\s*/)
: Array.isArray(this.categories)
? this.categories as string[]
: []
parsedCategories (): CalendarCategory[] {
return getParsedCategories(this.categories, this.categoryText)
},
},

Expand Down Expand Up @@ -294,10 +291,10 @@ export default CalendarWithEvents.extend({
timestampToDate (timestamp: CalendarTimestamp): Date {
return timestampToDate(timestamp)
},
getCategoryList (categories: string[]): string[] {
getCategoryList (categories: CalendarCategory[]): CalendarCategory[] {
if (!this.noEvents) {
const categoryMap = categories.reduce((map, category, index) => {
map[category] = { index, count: 0 }
if (typeof category === 'object' && category.categoryName) map[category.categoryName] = { index, count: 0 }

return map
}, Object.create(null))
Expand Down Expand Up @@ -335,9 +332,13 @@ export default CalendarWithEvents.extend({
}
}

categories = Object.keys(categoryMap)
categories = categories.filter((category: CalendarCategory) => {
if (typeof category === 'object' && category.categoryName) {
return categoryMap.hasOwnProperty(category.categoryName)
}
return false
})
}

return categories
},
},
Expand Down
79 changes: 61 additions & 18 deletions packages/vuetify/src/components/VCalendar/VCalendarCategory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import { VNode } from 'vue'
import VCalendarDaily from './VCalendarDaily'

// Util
import { getSlot } from '../../util/helpers'
import { CalendarTimestamp } from 'types'
import { convertToUnit, getSlot } from '../../util/helpers'
import { CalendarCategory, CalendarTimestamp } from 'types'
import props from './util/props'
import { getParsedCategories } from './util/parser'

/* @vue/component */
export default VCalendarDaily.extend({
Expand All @@ -26,15 +27,10 @@ export default VCalendarDaily.extend({
...this.themeClasses,
}
},
parsedCategories (): string[] {
return typeof this.categories === 'string' && this.categories
? this.categories.split(/\s*,\s*/)
: Array.isArray(this.categories)
? this.categories as string[]
: []
parsedCategories (): CalendarCategory[] {
return getParsedCategories(this.categories, this.categoryText)
},
},

methods: {
genDayHeader (day: CalendarTimestamp, index: number): VNode[] {
const data = {
Expand All @@ -44,14 +40,18 @@ export default VCalendarDaily.extend({
week: this.days, ...day, index,
}

const children = this.parsedCategories.map(category => this.genDayHeaderCategory(day, this.getCategoryScope(scope, category)))
const children = this.parsedCategories.map(category => {
return this.genDayHeaderCategory(day, this.getCategoryScope(scope, category))
})

return [this.$createElement('div', data, children)]
},
getCategoryScope (scope: any, category: string) {
getCategoryScope (scope: any, category: CalendarCategory) {
const cat = typeof category === 'object' && category &&
category.categoryName === this.categoryForInvalid ? null : category
return {
...scope,
category: category === this.categoryForInvalid ? null : category,
category: cat,
}
},
genDayHeaderCategory (day: CalendarTimestamp, scope: any): VNode {
Expand All @@ -61,25 +61,68 @@ export default VCalendarDaily.extend({
return this.getCategoryScope(this.getSlotScope(day), scope.category)
}),
}, [
getSlot(this, 'category', scope) || this.genDayHeaderCategoryTitle(scope.category),
getSlot(this, 'category', scope) || this.genDayHeaderCategoryTitle(scope.category && scope.category.categoryName),
getSlot(this, 'day-header', scope),
])
},
genDayHeaderCategoryTitle (category: string) {
genDayHeaderCategoryTitle (categoryName: string | null) {
return this.$createElement('div', {
staticClass: 'v-calendar-category__category',
}, category === null ? this.categoryForInvalid : category)
}, categoryName === null ? this.categoryForInvalid : categoryName)
},
genDays (): VNode[] {
const d = this.days[0]
let days = this.days.slice()
days = new Array(this.parsedCategories.length)
days.fill(d)
return days.map((v, i) => this.genDay(v, 0, i))
},
genDay (day: CalendarTimestamp, index: number, categoryIndex: number): VNode {
const category = this.parsedCategories[categoryIndex]
return this.$createElement('div', {
key: day.date + '-' + categoryIndex,
staticClass: 'v-calendar-daily__day',
class: this.getRelativeClasses(day),
on: this.getDefaultMouseEventHandlers(':time', e => {
return this.getSlotScope(this.getTimestampAtEvent(e, day))
}),
}, [
...this.genDayIntervals(index, category),
...this.genDayBody(day, category),
])
},
genDayIntervals (index: number, category: CalendarCategory): VNode[] {
return this.intervals[index].map(v => this.genDayInterval(v, category))
},
genDayInterval (interval: CalendarTimestamp, category: CalendarCategory): VNode {
const height: string | undefined = convertToUnit(this.intervalHeight)
const styler = this.intervalStyle || this.intervalStyleDefault

const data = {
key: interval.time,
staticClass: 'v-calendar-daily__day-interval',
style: {
height,
...styler({ ...interval, category }),
},
}

const children = getSlot(this, 'interval', () =>
this.getCategoryScope(this.getSlotScope(interval), category)
)

return this.$createElement('div', data, children)
},
genDayBody (day: CalendarTimestamp): VNode[] {
genDayBody (day: CalendarTimestamp, category: CalendarCategory): VNode[] {
const data = {
staticClass: 'v-calendar-category__columns',
}

const children = this.parsedCategories.map(category => this.genDayBodyCategory(day, category))
const children = [this.genDayBodyCategory(day, category)]

return [this.$createElement('div', data, children)]
},
genDayBodyCategory (day: CalendarTimestamp, category: string): VNode {
genDayBodyCategory (day: CalendarTimestamp, category: CalendarCategory): VNode {
const data = {
staticClass: 'v-calendar-category__column',
on: this.getDefaultMouseEventHandlers(':time-category', e => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {
mount,
Wrapper,
MountOptions,
} from '@vue/test-utils'
import { ExtractVue } from '../../../util/mixins'
import VCalendarCategory from '../VCalendarCategory'

describe('VCalendarCategory', () => {
type Instance = ExtractVue<typeof VCalendarCategory>
let mountFunction: (options?: MountOptions<Instance>) => Wrapper<Instance>
beforeEach(() => {
mountFunction = (options?: MountOptions<Instance>) => {
return mount(VCalendarCategory, {
...options,
mocks: {
$vuetify: {
lang: {
current: 'en-US',
},
},
},
})
}
})

it('should test categoryText prop as a string', async () => {
const wrapper = mountFunction({
propsData: {
categories: [{ name: 'Nate' }],
categoryText: 'name',
},
})

expect(wrapper.find('.v-calendar-category__column-header').text()).toEqual('Nate')
})

it('should test categoryText prop as a function', async () => {
const wrapper = mountFunction({
propsData: {
categories: [{ name: 'Nate', age: 20 }],
categoryText (category) {
return category.age
},
},
})

expect(wrapper.find('.v-calendar-category__column-header').text()).toEqual('20')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,15 @@ describe('calendar-with-events.ts', () => {

expect(wrapper.vm.eventColorFunction).toBeDefined()
expect(typeof wrapper.vm.eventColorFunction).toBe('function')
expect(wrapper.vm.eventColorFunction()).toBe('green')
expect(wrapper.vm.eventColorFunction({})).toBe('green')

wrapper.setProps({
eventColor: 'red',
})

expect(wrapper.vm.eventColorFunction).toBeDefined()
expect(typeof wrapper.vm.eventColorFunction).toBe('function')
expect(wrapper.vm.eventColorFunction()).toBe('red')
expect(wrapper.vm.eventColorFunction({})).toBe('red')
})

it('should work with event text colors', async () => {
Expand All @@ -80,15 +80,15 @@ describe('calendar-with-events.ts', () => {

expect(wrapper.vm.eventTextColorFunction).toBeDefined()
expect(typeof wrapper.vm.eventTextColorFunction).toBe('function')
expect(wrapper.vm.eventTextColorFunction()).toBe('green')
expect(wrapper.vm.eventTextColorFunction({})).toBe('green')

wrapper.setProps({
eventTextColor: 'red',
})

expect(wrapper.vm.eventTextColorFunction).toBeDefined()
expect(typeof wrapper.vm.eventTextColorFunction).toBe('function')
expect(wrapper.vm.eventTextColorFunction()).toBe('red')
expect(wrapper.vm.eventTextColorFunction({})).toBe('red')
})

it('should work with event names', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,6 @@ describe('calendar-with-intervals.ts', () => {
expect(wrapper.vm.timeToY('23:50')).toBe(240)

expect(wrapper.vm.timeToY('00:05')).toBe(0)

expect(Math.round(wrapper.vm.timeToY('08:30', false) || 0)).toBe(2208)
expect(wrapper.vm.timeToY('09:30', false)).toBe(2496)
expect(wrapper.vm.timeToY('23:50', false)).toBe(6624)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
CalendarEventOverlapMode,
CalendarEvent,
CalendarEventCategoryFunction,
CalendarCategory,
} from 'vuetify/types'

// Types
Expand Down Expand Up @@ -80,7 +81,11 @@ export default CalendarBase.extend({
ripple,
},

props: props.events,
props: {
...props.events,
...props.calendar,
...props.category,
},

computed: {
noEvents (): boolean {
Expand All @@ -92,11 +97,6 @@ export default CalendarBase.extend({
parsedEventOverlapThreshold (): number {
return parseInt(this.eventOverlapThreshold)
},
eventColorFunction (): CalendarEventColorFunction {
return typeof this.eventColor === 'function'
? this.eventColor
: () => (this.eventColor as string)
},
eventTimedFunction (): CalendarEventTimedFunction {
return typeof this.eventTimed === 'function'
? this.eventTimed
Expand Down Expand Up @@ -126,11 +126,16 @@ export default CalendarBase.extend({
return this.parsedWeekdays
},
categoryMode (): boolean {
return false
return this.type === 'category'
},
},

methods: {
eventColorFunction (e: CalendarEvent): string {
return typeof this.eventColor === 'function'
? this.eventColor(e)
: e.color || this.eventColor
},
parseEvent (input: CalendarEvent, index = 0): CalendarEventParsed {
return parseEvent(
input,
Expand Down Expand Up @@ -396,9 +401,10 @@ export default CalendarBase.extend({
event => isEventOverlapping(event, start, end)
)
},
isEventForCategory (event: CalendarEventParsed, category: string | undefined | null): boolean {
isEventForCategory (event: CalendarEventParsed, category: CalendarCategory): boolean {
return !this.categoryMode ||
category === event.category ||
(typeof category === 'object' && category.calendarName &&
category.categoryName === event.category) ||
(typeof event.category !== 'string' && category === null)
},
getEventsForDay (day: CalendarDaySlotScope): CalendarEventParsed[] {
Expand Down
28 changes: 28 additions & 0 deletions packages/vuetify/src/components/VCalendar/util/parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { CalendarCategory, CalendarCategoryTextFunction } from 'types'

export function parsedCategoryText (
category: CalendarCategory,
categoryText: string | CalendarCategoryTextFunction
): string {
return typeof categoryText === 'string' && typeof category === 'object' && category
? category[categoryText]
: typeof categoryText === 'function'
? categoryText(category)
: category
}

export function getParsedCategories (
categories: CalendarCategory | CalendarCategory[],
categoryText: string | CalendarCategoryTextFunction
): CalendarCategory[] {
if (typeof categories === 'string') return categories.split(/\s*,\s/)
if (Array.isArray(categories)) {
return categories.map((v: CalendarCategory) => {
const categoryName = typeof v === 'string' ? v
: typeof v === 'object' && v && typeof v.categoryName === 'string' ? v.categoryName
: parsedCategoryText(v, categoryText)
return { categoryName }
})
}
return []
}

0 comments on commit 806864c

Please sign in to comment.