v-calendar
Version:
A calendar and date picker plugin for Vue.js.
213 lines (198 loc) • 5.38 kB
text/typescript
import type { ComponentPublicInstance, Directive, DirectiveBinding } from 'vue';
import type { Placement } from '@popperjs/core';
import { elementContains, on, resolveEl } from './helpers';
export type PopoverVisibility = 'click' | 'hover' | 'hover-focus' | 'focus';
export interface PopoverOptions {
id: string;
visibility: PopoverVisibility;
isInteractive: boolean;
autoHide: boolean;
force: boolean;
target: unknown;
placement: Placement;
modifiers: any;
data: any;
showDelay: number;
hideDelay: number;
}
export interface PopoverState {
isVisible: boolean;
target: unknown;
data: any;
transition: string;
placement: Placement;
direction: string;
positionFixed: false;
modifiers: any[];
isInteractive: boolean;
visibility: PopoverVisibility;
isHovered: boolean;
isFocused: boolean;
autoHide: boolean;
force: boolean;
}
export interface PopoverEvent {
detail: Partial<PopoverOptions>;
}
export interface PopoverEventHandlers {
click: (e: MouseEvent) => void;
mousemove: (e: MouseEvent) => void;
mouseleave: (e: MouseEvent) => void;
focusin: (e: MouseEvent) => void;
focusout: (e: MouseEvent) => void;
}
export function showPopover(opts: Partial<PopoverOptions>) {
if (document) {
document.dispatchEvent(
new CustomEvent('show-popover', {
detail: opts,
}),
);
}
}
export function hidePopover(opts: Partial<PopoverOptions>) {
if (document) {
document.dispatchEvent(
new CustomEvent('hide-popover', {
detail: opts,
}),
);
}
}
export function togglePopover(opts: Partial<PopoverOptions>) {
if (document) {
document.dispatchEvent(
new CustomEvent('toggle-popover', {
detail: opts,
}),
);
}
}
export function getPopoverEventHandlers(
opts: Partial<PopoverOptions>,
): Partial<PopoverEventHandlers> {
const { visibility } = opts;
const click = visibility === 'click';
const hover = visibility === 'hover';
const hoverFocus = visibility === 'hover-focus';
const focus = visibility === 'focus';
opts.autoHide = !click;
let hovered = false;
let focused = false;
const clickHandler = (e: MouseEvent) => {
if (click) {
togglePopover({
...opts,
target: opts.target || (e.currentTarget as HTMLElement),
});
e.stopPropagation();
}
};
const mouseMoveHandler = (e: MouseEvent) => {
if (!hovered) {
hovered = true;
if (hover || hoverFocus) {
showPopover({
...opts,
target: opts.target || (e.currentTarget as HTMLElement),
});
}
}
};
const mouseLeaveHandler = () => {
if (hovered) {
hovered = false;
if (hover || (hoverFocus && !focused)) {
hidePopover(opts);
}
}
};
const focusInHandler = (e: FocusEvent) => {
if (!focused) {
focused = true;
if (focus || hoverFocus) {
showPopover({
...opts,
target: opts.target || (e.currentTarget as HTMLElement),
});
}
}
};
const focusOutHandler = (e: FocusEvent) => {
if (
focused &&
!elementContains(e.currentTarget as Node, e.relatedTarget as Element)
) {
focused = false;
if (focus || (hoverFocus && !hovered)) {
hidePopover(opts);
}
}
};
const handlers: Record<string, Function> = {};
switch (opts.visibility) {
case 'click':
handlers.click = clickHandler;
break;
case 'hover':
handlers.mousemove = mouseMoveHandler;
handlers.mouseleave = mouseLeaveHandler;
break;
case 'focus':
handlers.focusin = focusInHandler;
handlers.focusout = focusOutHandler;
break;
case 'hover-focus':
handlers.mousemove = mouseMoveHandler;
handlers.mouseleave = mouseLeaveHandler;
handlers.focusin = focusInHandler;
handlers.focusout = focusOutHandler;
break;
}
return handlers;
}
const removeHandlers = (target: Element | ComponentPublicInstance | string) => {
const el = resolveEl(target);
if (el == null) return;
const handlers = (el as any).popoverHandlers;
if (!handlers || !handlers.length) return;
handlers.forEach((handler: Function) => handler());
delete (el as any).popoverHandlers;
};
const addHandlers = (
target: Element | ComponentPublicInstance | string,
opts: Partial<PopoverOptions>,
) => {
const el = resolveEl(target);
if (el == null) return;
const remove: Function[] = [];
const handlers = getPopoverEventHandlers(opts);
Object.entries(handlers).forEach(([event, handler]) => {
remove.push(on(el, event, handler as EventListener));
});
(el as any).popoverHandlers = remove;
};
export const popoverDirective: Directive = {
mounted(el: any, binding: DirectiveBinding<PopoverOptions>) {
const { value } = binding;
if (!value) return;
addHandlers(el, value);
},
updated(el: any, binding: DirectiveBinding<PopoverOptions>) {
const { oldValue, value } = binding;
const oldVisibility = oldValue?.visibility;
const newVisibility = value?.visibility;
if (oldVisibility !== newVisibility) {
if (oldVisibility) {
removeHandlers(el);
if (!newVisibility) hidePopover(oldValue);
}
if (newVisibility) {
addHandlers(el, value);
}
}
},
unmounted(el: Element) {
removeHandlers(el);
},
};