vuikit
Version:
A Vuejs component library based on UIkit
299 lines (239 loc) • 6.22 kB
JavaScript
/**
* Vuikit 0.7.0
* (c) 2018 Miljan Aleksic
* @license MIT
*/
import css from 'vuikit/core/helpers/css'
import { warn } from 'vuikit/core/helpers/debug'
import { on, off } from 'vuikit/core/helpers/dom/event'
import { Animation } from 'vuikit/core/helpers/dom/animation'
import { addClass, removeClass } from 'vuikit/core/helpers/dom/class'
import { get, isEmpty, includes, isObject, toInteger } from 'vuikit/core/util'
import { positionAt, flipPosition, getPositionAxis } from 'vuikit/core/helpers/dom/position'
let delayedShow
let tooltip = {}
const uid = 'v-tooltip'
const positions = [
'top',
'top-left',
'top-center',
'top-right',
'bottom',
'bottom-left',
'bottom-center',
'bottom-right',
'left',
'left-center',
'right',
'right-center'
]
export default {
inserted (target, binding, vnode) {
const ctx = getContext(target, binding, vnode)
if (ctx) {
setEvents(ctx)
}
},
componentUpdated (target, binding, vnode) {
const ctx = getContext(target, binding, vnode)
if (ctx) {
setEvents(ctx)
updateVisibles(ctx)
}
},
unbind (target, binding) {
hide()
removeEvents(target)
}
}
/**
* SET / REMOVE events
**/
function setEvents (ctx) {
const { triggers } = ctx.props
removeEvents(ctx)
if (triggers.match(/click/)) {
on(ctx.target, 'click', () => toggle(ctx), uid)
}
if (triggers.match(/hover/)) {
on(ctx.target, 'mouseenter', () => show(ctx), uid)
on(ctx.target, 'mouseleave', () => hide(ctx), uid)
}
if (triggers.match(/focus/)) {
on(ctx.target, 'focusin', () => show(ctx), uid)
on(ctx.target, 'focusout', () => hide(ctx), uid)
}
}
function removeEvents (ctx) {
off(ctx.target, 'click mouseenter mouseleave focusin focusout', uid)
}
/**
* SHOW / HIDE the tooltip
**/
function show (ctx) {
const { props } = ctx
const { outer, inner } = getTooltip()
inner.innerHTML = props.content
delayedShow = setTimeout(() => {
// append tooltip at $root as the styles
// could be scoped to the app dom
const $root = ctx.vnode.context.$root.$el
$root.appendChild(outer)
const { dir, align } = positionTooltip(ctx)
Animation.in({
element: outer,
duration: props.duration,
origin: `${dir}-${align}`,
animation: props.animationIn
})
}, props.delay)
}
function hide (ctx) {
const { outer } = getTooltip()
clearTimeout(delayedShow)
removeClass(outer, 'uk-active')
// remove from dom
if (outer.parentNode) {
outer.parentNode.removeChild(outer)
}
// force recreating tooltip each time as in
// edge situations redrawing doesn't work well
tooltip = {}
}
function toggle (ctx) {
isEmpty(tooltip)
? show(ctx)
: hide(ctx)
}
/**
* Update visible tooltips
**/
function updateVisibles (ctx) {
// abort if no tooltip to update
if (isEmpty(tooltip)) {
return
}
const props = getProps(ctx)
const { inner } = getTooltip()
inner.innerHTML = props.content
positionTooltip(ctx)
}
/**
* Position tooltip
**/
function positionTooltip (ctx) {
const { target, props } = ctx
const { outer: tooltip } = getTooltip()
const { position, offset, boundary, flip } = props
let [dir, align = 'center'] = position.split('-')
// remove any position class
const classesRx = new RegExp(`uk-tooltip-(top|bottom|left|right)(-[a-z]+)?`)
tooltip.className = tooltip.className.replace(classesRx, '')
// reset pos
css(tooltip, { top: '', left: '' })
const axis = getPositionAxis(position)
const elAttach = axis === 'x'
? `${flipPosition(dir)} ${align}`
: `${align} ${flipPosition(dir)}`
const targetAttach = axis === 'x'
? `${dir} ${align}`
: `${align} ${dir}`
const elOffset = axis === 'x'
? `${dir === 'left' ? -1 * offset : offset}`
: `${dir === 'top' ? -1 * offset : offset}`
const { x, y } = positionAt({
flip,
target,
boundary,
elAttach,
elOffset,
element: tooltip,
targetAttach,
targetOffset: null
}).target
dir = axis === 'x' ? x : y
align = axis === 'x' ? y : x
addClass(tooltip, `uk-tooltip-${dir}-${align} uk-active`)
return {
dir,
align
}
}
/**
* Get tooltip props
**/
function getProps (ctx) {
const { arg, value, vnode } = ctx.binding
let delay = 1
let offset = 0
let flip = true
let content = null
let duration = 100
let position = 'top'
let boundary = window
let animation = 'scale-up'
let triggers = 'hover focus'
if (isObject(value)) {
content = value.content
flip = get(value, 'flip', flip)
delay = get(value, 'delay', delay)
offset = toInteger(offset) || offset
boundary = value.boundary || boundary
duration = get(value, 'duration', duration)
triggers = get(value, 'triggers', triggers)
position = value.position || arg || position
animation = get(value, 'animation', animation)
} else {
content = value
position = arg || position
}
if (!includes(positions, position)) {
warn('Invalid v-tooltip position', vnode)
return false
}
if (!content) {
warn('Invalid v-tooltip content', vnode)
return false
}
// decompose animation
const animations = animation.split(',')
const animationIn = prefixAnimations(animations[0])
return { delay, offset, flip, content, position, boundary, animationIn, duration, triggers }
}
/**
* Prefix all animations (separated by space) with `uk-animation-`
**/
function prefixAnimations (str) {
if (!str.trim()) {
return ''
}
return str.match(/[\w-]+/g).map(v => `uk-animation-${v}`).join(' ')
}
/**
* Get or create the tooltip element
**/
function getTooltip () {
if (!isEmpty(tooltip)) {
return tooltip
}
const outer = document.createElement('div')
const inner = document.createElement('div')
addClass(outer, 'uk-tooltip')
addClass(inner, 'uk-tooltip-inner')
outer.appendChild(inner)
tooltip.outer = outer
tooltip.inner = inner
return tooltip
}
/**
* Get the context used across
**/
function getContext (target, binding, vnode) {
const ctx = { target, binding, vnode }
ctx.props = getProps(ctx)
if (!ctx.props) {
binding.def.unbind(target, binding)
return
}
return ctx
}