UNPKG

vuetify

Version:

Vue Material Component Framework

256 lines (212 loc) 7.79 kB
import { CalendarEventOverlapMode, CalendarEventVisual } from 'vuetify/types' import { getOverlapGroupHandler, getVisuals, hasOverlap, getNormalizedRange } from './common' import { getTimestampIdentifier } from '../util/timestamp' interface Group { start: number end: number visuals: CalendarEventVisual[] } interface Node { parent: Node | null sibling: boolean index: number visual: CalendarEventVisual start: number end: number children: Node[] } const FULL_WIDTH = 100 const DEFAULT_OFFSET = 5 const WIDTH_MULTIPLIER = 1.7 /** * Variation of column mode where events can be stacked. The priority of this * mode is to stack events together taking up the least amount of space while * trying to ensure the content of the event is always visible as well as its * start and end. A sibling column has intersecting event content and must be * placed beside each other. Non-sibling columns are offset by 5% from the * previous column. The width is scaled by 1.7 so the events overlap and * whitespace is reduced. If there is a hole in columns the event width is * scaled up so it intersects with the next column. The columns have equal * width in the space they are given. If the event doesn't have any to the * right of it that intersect with it's content it's right side is extended * to the right side. */ export const stack: CalendarEventOverlapMode = (events, firstWeekday, overlapThreshold) => { const handler = getOverlapGroupHandler(firstWeekday) // eslint-disable-next-line max-statements return (day, dayEvents, timed, reset) => { if (!timed) { return handler.getVisuals(day, dayEvents, timed, reset) } const dayStart = getTimestampIdentifier(day) const visuals = getVisuals(dayEvents, dayStart) const groups = getGroups(visuals, dayStart) for (const group of groups) { const nodes: Node[] = [] for (const visual of group.visuals) { const child = getNode(visual, dayStart) const index = getNextIndex(child, nodes) if (index === false) { const parent = getParent(child, nodes) if (parent) { child.parent = parent child.sibling = hasOverlap(child.start, child.end, parent.start, addTime(parent.start, overlapThreshold)) child.index = parent.index + 1 parent.children.push(child) } } else { const [parent] = getOverlappingRange(child, nodes, index - 1, index - 1) const children = getOverlappingRange(child, nodes, index + 1, index + nodes.length, true) child.children = children child.index = index if (parent) { child.parent = parent child.sibling = hasOverlap(child.start, child.end, parent.start, addTime(parent.start, overlapThreshold)) parent.children.push(child) } for (const grand of children) { if (grand.parent === parent) { grand.parent = child } const grandNext = grand.index - child.index <= 1 if (grandNext && child.sibling && hasOverlap(child.start, addTime(child.start, overlapThreshold), grand.start, grand.end)) { grand.sibling = true } } } nodes.push(child) } calculateBounds(nodes, overlapThreshold) } visuals.sort((a, b) => (a.left - b.left) || (a.event.startTimestampIdentifier - b.event.startTimestampIdentifier)) return visuals } } function calculateBounds (nodes: Node[], overlapThreshold: number) { for (const node of nodes) { const { visual, parent } = node const columns = getMaxChildIndex(node) + 1 const spaceLeft = parent ? parent.visual.left : 0 const spaceWidth = FULL_WIDTH - spaceLeft const offset = Math.min(DEFAULT_OFFSET, FULL_WIDTH / columns) const columnWidthMultiplier = getColumnWidthMultiplier(node, nodes) const columnOffset = spaceWidth / (columns - node.index + 1) const columnWidth = spaceWidth / (columns - node.index + (node.sibling ? 1 : 0)) * columnWidthMultiplier if (parent) { visual.left = node.sibling ? spaceLeft + columnOffset : spaceLeft + offset } visual.width = hasFullWidth(node, nodes, overlapThreshold) ? FULL_WIDTH - visual.left : Math.min(FULL_WIDTH - visual.left, columnWidth * WIDTH_MULTIPLIER) } } function getColumnWidthMultiplier (node: Node, nodes: Node[]): number { if (!node.children.length) { return 1 } const maxColumn = node.index + nodes.length const minColumn = node.children.reduce((min, c) => Math.min(min, c.index), maxColumn) return minColumn - node.index } function getOverlappingIndices (node: Node, nodes: Node[]): number[] { const indices: number[] = [] for (const other of nodes) { if (hasOverlap(node.start, node.end, other.start, other.end)) { indices.push(other.index) } } return indices } function getNextIndex (node: Node, nodes: Node[]): number | false { const indices = getOverlappingIndices(node, nodes) indices.sort() for (let i = 0; i < indices.length; i++) { if (i < indices[i]) { return i } } return false } function getOverlappingRange (node: Node, nodes: Node[], indexMin: number, indexMax: number, returnFirstColumn = false): Node[] { const overlapping: Node[] = [] for (const other of nodes) { if (other.index >= indexMin && other.index <= indexMax && hasOverlap(node.start, node.end, other.start, other.end)) { overlapping.push(other) } } if (returnFirstColumn && overlapping.length > 0) { const first = overlapping.reduce((min, n) => Math.min(min, n.index), overlapping[0].index) return overlapping.filter(n => n.index === first) } return overlapping } function getParent (node: Node, nodes: Node[]): Node | null { let parent: Node | null = null for (const other of nodes) { if (hasOverlap(node.start, node.end, other.start, other.end) && (parent === null || other.index > parent.index)) { parent = other } } return parent } function hasFullWidth (node: Node, nodes: Node[], overlapThreshold: number): boolean { for (const other of nodes) { if (other !== node && other.index > node.index && hasOverlap(node.start, addTime(node.start, overlapThreshold), other.start, other.end)) { return false } } return true } function getGroups (visuals: CalendarEventVisual[], dayStart: number): Group[] { const groups: Group[] = [] for (const visual of visuals) { const [start, end] = getNormalizedRange(visual.event, dayStart) let added = false for (const group of groups) { if (hasOverlap(start, end, group.start, group.end)) { group.visuals.push(visual) group.end = Math.max(group.end, end) added = true break } } if (!added) { groups.push({ start, end, visuals: [visual] }) } } return groups } function getNode (visual: CalendarEventVisual, dayStart: number): Node { const [start, end] = getNormalizedRange(visual.event, dayStart) return { parent: null, sibling: true, index: 0, visual, start, end, children: [], } } function getMaxChildIndex (node: Node): number { let max = node.index for (const child of node.children) { const childMax = getMaxChildIndex(child) if (childMax > max) { max = childMax } } return max } function addTime (identifier: number, minutes: number): number { const removeMinutes = identifier % 100 const totalMinutes = removeMinutes + minutes const addHours = Math.floor(totalMinutes / 60) const addMinutes = totalMinutes % 60 return identifier - removeMinutes + addHours * 100 + addMinutes }