vue-tippy
Version:
[](https://www.npmjs.com/package/vue-tippy) [](https://vuejs.org/) [](https://www
294 lines (245 loc) • 6.33 kB
text/typescript
import tippy, { Instance, Props, Content } from 'tippy.js'
import {
ref,
onMounted,
Ref,
isRef,
isReactive,
isVNode,
watch,
VNode,
h,
onUnmounted,
getCurrentInstance,
createApp,
shallowRef,
App,
} from 'vue'
import { TippyOptions, TippyContent } from '../types'
tippy.setDefaultProps({
//@ts-ignore
onShow: instance => {
if (!instance.props.content) return false
},
})
const isComponentInstance = (value: any): value is { $el: any } => {
return value instanceof Object && '$' in value && '$el' in value
}
type TippyElement = Element | any // TODO: use ComponentPublicInstance
export function useTippy(
el: TippyElement | (() => TippyElement) | Ref<TippyElement> | Ref<TippyElement | undefined>,
opts: TippyOptions = {},
settings: {
mount: boolean,
appName: string,
} = { mount: true, appName: 'Tippy' }
) {
settings = Object.assign({ mount: true, appName: 'Tippy' }, settings);
const vm = getCurrentInstance()
const instance = ref<Instance>()
const state = ref({
isEnabled: false,
isVisible: false,
isDestroyed: false,
isMounted: false,
isShown: false,
})
const headlessApp = shallowRef<App>()
let container: any = null
const getContainer = () => {
if (container) return container
container = document.createDocumentFragment()
return container
}
const getContent = (content: TippyContent): Content => {
let newContent: Content
let unwrappedContent: Content | VNode | { render: Function } = isRef(
content
)
? content.value
: content
if (isVNode(unwrappedContent)) {
if (!headlessApp.value) {
headlessApp.value = createApp({
name: settings.appName,
setup: () => {
return () => isRef(content) ? content.value : content
},
})
if (vm) {
Object.assign(headlessApp.value._context, vm.appContext)
}
headlessApp.value.mount(getContainer())
}
newContent = () => getContainer()
} else if (typeof unwrappedContent === 'object') {
if (!headlessApp.value) {
headlessApp.value = createApp({
name: settings.appName,
setup: () => {
return () => h(isRef(content) ? content.value : content as any)
},
})
if (vm) {
Object.assign(headlessApp.value._context, vm.appContext)
}
headlessApp.value.mount(getContainer())
}
newContent = () => getContainer()
} else {
newContent = unwrappedContent
}
return newContent!
}
const getProps = (opts: TippyOptions): Partial<Props> => {
let options: any = {}
if (isRef(opts)) {
options = opts.value || {}
} else if (isReactive(opts)) {
options = { ...opts }
} else {
options = { ...opts }
}
if (options.content) {
options.content = getContent(options.content)
}
if (options.triggerTarget) {
options.triggerTarget = isRef(options.triggerTarget)
? options.triggerTarget.value
: options.triggerTarget
}
if (!options.plugins || !Array.isArray(options.plugins)) {
options.plugins = []
}
options.plugins = options.plugins.filter((plugin: any) => plugin.name !== 'vueTippyReactiveState');
options.plugins.push({
name: 'vueTippyReactiveState',
fn: () => {
return {
onCreate() {
state.value.isEnabled = true
},
onMount() {
state.value.isMounted = true
},
onShow() {
state.value.isMounted = true
state.value.isVisible = true
},
onShown() {
state.value.isShown = true
},
onHide() {
state.value.isMounted = false
state.value.isVisible = false
},
onHidden() {
state.value.isShown = false
},
onUnmounted() {
state.value.isMounted = false
},
onDestroy() {
state.value.isDestroyed = true
},
}
}
})
return options as Props
}
const refresh = () => {
if (!instance.value) return
instance.value.setProps(getProps(opts))
}
const refreshContent = () => {
if (!instance.value || !opts.content) return
instance.value.setContent(getContent(opts.content))
}
const setContent = (value: TippyContent) => {
instance.value?.setContent(getContent(value))
}
const setProps = (value: TippyOptions) => {
instance.value?.setProps(getProps(value))
}
const destroy = () => {
if (instance.value) {
try {
//@ts-ignore
// delete instance.value.reference.$tippy
} catch (error) { }
instance.value.destroy()
instance.value = undefined
}
container = null
headlessApp.value?.unmount()
headlessApp.value = undefined
}
const show = () => {
instance.value?.show()
}
const hide = () => {
instance.value?.hide()
}
const disable = () => {
instance.value?.disable()
state.value.isEnabled = false;
}
const enable = () => {
instance.value?.enable()
state.value.isEnabled = true;
}
const unmount = () => {
instance.value?.unmount()
}
const mount = () => {
if (!el) return
let target = isRef(el) ? el.value : el
if (typeof target === 'function') target = target()
if (isComponentInstance(target)) {
target = target.$el as Element
}
if (target) {
//@ts-ignore
instance.value = tippy(target, getProps(opts))
//@ts-ignore
target.$tippy = response
}
}
const response = {
tippy: instance,
refresh,
refreshContent,
setContent,
setProps,
destroy,
hide,
show,
disable,
enable,
unmount,
mount,
state,
}
if (settings.mount) {
if (vm) {
if (vm.isMounted) {
mount()
} else {
onMounted(mount)
}
} else {
mount()
}
}
if (vm) {
onUnmounted(() => {
destroy()
})
}
if (isRef(opts) || isReactive(opts)) {
watch(opts, refresh, { immediate: false })
} else if (isRef(opts.content)) {
watch(opts.content, refreshContent, { immediate: false })
}
return response
}