Skip to content
This repository was archived by the owner on Apr 17, 2022. It is now read-only.

Commit 43fab28

Browse files
committed
refactor: clickOutside directive change, ref (#30)
1 parent 73cc008 commit 43fab28

File tree

3 files changed

+108
-43
lines changed

3 files changed

+108
-43
lines changed

src/Vue3DatePicker/Vue3DatePicker.vue

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<template>
22
<div :class="wrapperClass">
33
<DatepickerInput
4+
ref="inputRef"
45
v-bind="{
56
placeholder,
67
hideInputIcon,
@@ -29,7 +30,7 @@
2930
<teleport :to="teleport" :disabled="inline" v-if="isOpen">
3031
<DatepickerMenu
3132
v-if="isOpen"
32-
v-click-outside-directive.dp__menu="closeMenu"
33+
ref="dpMenuRef"
3334
:class="theme"
3435
:style="menuPosition"
3536
v-bind="{
@@ -113,13 +114,14 @@
113114
WeekStartNum,
114115
WeekStartStr,
115116
} from './interfaces';
116-
import { clickOutsideDirective as vClickOutsideDirective } from './directives/clickOutside';
117+
// import { clickOutsideDirective as vClickOutsideDirective } from './directives/clickOutside';
117118
import { getDateHours, getDateMinutes, getDefaultPattern } from './utils/date-utils';
118119
import { getDefaultTextInputOptions, getDefaultFilters } from './utils/util';
119120
import { usePosition } from './components/composition/position';
120121
import { useExternalInternalMapper } from './components/composition/external-internal-mapper';
121122
import { isString } from './utils/type-guard';
122123
import { mapSlots } from './components/composition/slots';
124+
import { onClickOutside } from './directives/clickOutside';
123125
124126
const emit = defineEmits(['update:modelValue', 'textSubmit', 'closed', 'cleared']);
125127
const props = defineProps({
@@ -197,6 +199,8 @@
197199
const slots = useSlots();
198200
const isOpen = ref(false);
199201
const modelValue = toRef(props, 'modelValue');
202+
const dpMenuRef = ref(null);
203+
const inputRef = ref(null);
200204
201205
onMounted(() => {
202206
parseExternalModelValue(props.modelValue);
@@ -398,6 +402,8 @@
398402
}
399403
};
400404
405+
onClickOutside(dpMenuRef, inputRef, closeMenu);
406+
401407
defineExpose({
402408
closeMenu,
403409
selectDate,
Lines changed: 81 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,82 @@
1-
import { DirectiveBinding } from 'vue';
2-
3-
interface CustomHTMLElement extends Partial<HTMLElement> {
4-
clickOutsideEvent: (ev: MouseEvent) => void;
5-
contains: (param: unknown) => boolean;
6-
}
7-
8-
export const clickOutsideDirective = {
9-
beforeMount(el: CustomHTMLElement, binding: DirectiveBinding): void {
10-
const em = binding.instance;
11-
const sameEl = Object.keys(binding.modifiers);
12-
13-
el.clickOutsideEvent = (event: MouseEvent) => {
14-
const path = event.composedPath ? event.composedPath() : undefined;
15-
16-
if (em && path && !path.includes(em.$el)) {
17-
if (sameEl.length > 0) {
18-
if (
19-
!sameEl.some((className) =>
20-
path.some((elm) => {
21-
if ((elm as Element).className && typeof (elm as Element)?.className === 'string') {
22-
return (elm as Element).className.includes(className);
23-
}
24-
}),
25-
)
26-
) {
27-
binding.value(event, el);
28-
}
29-
} else {
30-
if (!em.$el.contains(event.target as HTMLElement)) {
31-
binding.value(event, el);
32-
}
33-
}
34-
}
35-
};
36-
37-
document.body.addEventListener('click', el.clickOutsideEvent, false);
38-
},
39-
unmounted(el: CustomHTMLElement): void {
40-
document.body.removeEventListener('click', el.clickOutsideEvent);
41-
},
1+
// Following code is a port of @vueuse/core clickOutside hook
2+
import { unref, ComponentPublicInstance, watch, getCurrentScope, onScopeDispose } from 'vue';
3+
import { Fn, MaybeElementRef, MaybeRef, OnClickOutsideEvents, OnClickOutsideOptions } from '../interfaces';
4+
5+
const defaultWindow = typeof window !== 'undefined' ? window : undefined;
6+
7+
const noop = () => {
8+
return;
9+
};
10+
11+
const unrefElement = (elRef: MaybeElementRef): HTMLElement | SVGElement | undefined => {
12+
const plain = unref(elRef);
13+
return (plain as ComponentPublicInstance)?.$el ?? plain;
14+
};
15+
16+
const tryOnScopeDispose = (fn: Fn): boolean => {
17+
if (getCurrentScope()) {
18+
onScopeDispose(fn);
19+
return true;
20+
}
21+
return false;
22+
};
23+
24+
const useEventListener = (
25+
target: MaybeRef<EventTarget> | undefined,
26+
event: string,
27+
listener: EventListener,
28+
options: Record<string, boolean>,
29+
): (() => void) => {
30+
if (!target) return noop;
31+
32+
let cleanup = noop;
33+
34+
const stopWatch = watch(
35+
() => unref(target),
36+
(el) => {
37+
cleanup();
38+
if (!el) return;
39+
40+
el.addEventListener(event, listener, options);
41+
42+
cleanup = () => {
43+
el.removeEventListener(event, listener, options);
44+
cleanup = noop;
45+
};
46+
},
47+
{ immediate: true, flush: 'post' },
48+
);
49+
50+
const stop = () => {
51+
stopWatch();
52+
cleanup();
53+
};
54+
55+
tryOnScopeDispose(stop);
56+
57+
return stop;
58+
};
59+
60+
export const onClickOutside = <E extends keyof OnClickOutsideEvents = 'pointerdown'>(
61+
target: MaybeElementRef,
62+
inputRef: MaybeElementRef,
63+
handler: (evt: OnClickOutsideEvents[E]) => void,
64+
options: OnClickOutsideOptions<E> = {},
65+
): (() => void) | undefined => {
66+
const { window = defaultWindow, event = 'pointerdown' } = options;
67+
68+
if (!window) return;
69+
70+
const listener = (event: OnClickOutsideEvents[E]) => {
71+
const el = unrefElement(target);
72+
const inputEl = unrefElement(inputRef);
73+
74+
if (!el || !inputEl) return;
75+
76+
if (el === event.target || event.composedPath().includes(el) || event.composedPath().includes(inputEl)) return;
77+
78+
handler(event);
79+
};
80+
81+
return useEventListener(window, event, listener as EventListener, { passive: true });
4282
};

src/Vue3DatePicker/interfaces.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { ComponentPublicInstance, Ref } from 'vue';
2+
13
export type DynamicClass = Record<string, boolean>;
24

35
export interface IDefaultSelect<T = number> {
@@ -107,3 +109,20 @@ export interface UseMonthYearPick {
107109
year: number;
108110
month: number;
109111
}
112+
113+
export type MaybeRef<T> = T | Ref<T>;
114+
export type Fn = () => void;
115+
116+
export interface ConfigurableWindow {
117+
window?: Window;
118+
}
119+
120+
export type MaybeElementRef = MaybeRef<HTMLElement | SVGElement | ComponentPublicInstance | undefined | null>;
121+
export type OnClickOutsideEvents = Pick<
122+
WindowEventMap,
123+
'click' | 'mousedown' | 'mouseup' | 'touchstart' | 'touchend' | 'pointerdown' | 'pointerup'
124+
>;
125+
126+
export interface OnClickOutsideOptions<E extends keyof OnClickOutsideEvents> extends ConfigurableWindow {
127+
event?: E;
128+
}

0 commit comments

Comments
 (0)