visbug-lib
Version:
<p align="center"> <img src="./assets/visbug.png" width="300" height="300" alt="visbug"> <br> <a href="https://www.npmjs.org/package/visbug"><img src="https://img.shields.io/npm/v/visbug.svg?style=flat" alt="npm latest version number"></a> <a href
276 lines (222 loc) • 7.31 kB
JavaScript
import $ from 'blingblingjs'
import hotkeys from 'hotkeys-js'
import { TinyColor } from '@ctrl/tinycolor'
import { queryPage } from './search'
import { getStyles, camelToDash, isOffBounds,
deepElementFromPoint, getShadowValues,
getTextShadowValues
} from '../utilities/'
const state = {
active: {
tip: null,
target: null,
},
tips: new Map(),
}
const services = {}
export function MetaTip({select}) {
services.selectors = {select}
$('body').on('mousemove', mouseMove)
$('body').on('click', togglePinned)
hotkeys('esc', _ => removeAll())
restorePinnedTips()
return () => {
$('body').off('mousemove', mouseMove)
$('body').off('click', togglePinned)
hotkeys.unbind('esc')
hideAll()
}
}
const mouseMove = e => {
const target = deepElementFromPoint(e.clientX, e.clientY)
if (isOffBounds(target) || target.nodeName === 'VISBUG-METATIP' || target.hasAttribute('data-metatip')) { // aka: mouse out
if (state.active.tip) {
wipe({
tip: state.active.tip,
e: {target: state.active.target},
})
clearActive()
}
return
}
toggleTargetCursor(e.altKey, target)
showTip(target, e)
}
export function showTip(target, e) {
if (!state.active.tip) { // create
const tip = render(target)
document.body.appendChild(tip)
positionTip(tip, e)
observe({tip, target})
state.active.tip = tip
state.active.target = target
}
else if (target == state.active.target) { // update position
// update position
positionTip(state.active.tip, e)
}
else { // update content
render(target, state.active.tip)
state.active.target = target
positionTip(state.active.tip, e)
}
}
export function positionTip(tip, e) {
const { north, west } = mouse_quadrant(e)
const { left, top } = tip_position(tip, e, north, west)
tip.style.left = left
tip.style.top = top
tip.style.setProperty('--arrow', north
? 'var(--arrow-up)'
: 'var(--arrow-down)')
tip.style.setProperty('--shadow-direction', north
? 'var(--shadow-up)'
: 'var(--shadow-down)')
tip.style.setProperty('--arrow-top', !north
? '-7px'
: 'calc(100% - 1px)')
tip.style.setProperty('--arrow-left', west
? 'calc(100% - 15px - 15px)'
: '15px')
}
const restorePinnedTips = () => {
state.tips.forEach(({tip}, target) => {
tip.style.display = 'block'
render(target, tip)
observe({tip, target})
})
}
export function hideAll() {
state.tips.forEach(({tip}, target) =>
tip.style.display = 'none')
if (state.active.tip) {
state.active.tip.remove()
clearActive()
}
}
export function removeAll() {
state.tips.forEach(({tip}, target) => {
tip.remove()
unobserve({tip, target})
})
$('[data-metatip]').attr('data-metatip', null)
state.tips.clear()
}
const render = (el, tip = document.createElement('visbug-metatip')) => {
const { width, height } = el.getBoundingClientRect()
const colormode = 'toHslString'; //$('vis-bug')[0].colorMode
const styles = getStyles(el)
.map(style => Object.assign(style, {
prop: camelToDash(style.prop)
}))
.filter(style =>
style.prop.includes('font-family')
? el.matches('h1,h2,h3,h4,h5,h6,p,a,date,caption,button,figcaption,nav,header,footer')
: true
)
.map(style => {
if (style.prop.includes('color') || style.prop.includes('background-color') || style.prop.includes('border-color') || style.prop.includes('Color') || style.prop.includes('fill') || style.prop.includes('stroke'))
style.value = `<span color style="background-color:${style.value};"></span>${new TinyColor(style.value)[colormode]()}`
if (style.prop.includes('box-shadow')) {
const [, color, x, y, blur, spread] = getShadowValues(style.value)
style.value = `${new TinyColor(color)[colormode]()} ${x} ${y} ${blur} ${spread}`
}
if (style.prop.includes('text-shadow')) {
const [, color, x, y, blur] = getTextShadowValues(style.value)
style.value = `${new TinyColor(color)[colormode]()} ${x} ${y} ${blur}`
}
if (style.prop.includes('font-family') && style.value.length > 25)
style.value = style.value.slice(0,25) + '...'
if (style.prop.includes('grid-template-areas'))
style.value = style.value.replace(/" "/g, '"<br>"')
if (style.prop.includes('background-image'))
style.value = `<a target="_blank" href="${style.value.slice(style.value.indexOf('(') + 2, style.value.length - 2)}">${style.value.slice(0,25) + '...'}</a>`
// check if style is inline style, show indicator
if (el.getAttribute('style') && el.getAttribute('style').includes(style.prop))
style.value = `<span local-change>${style.value}</span>`
return style
})
const localModifications = styles.filter(style =>
el.getAttribute('style') && el.getAttribute('style').includes(style.prop)
? 1
: 0)
const notLocalModifications = styles.filter(style =>
el.getAttribute('style') && el.getAttribute('style').includes(style.prop)
? 0
: 1)
tip.meta = {
el,
width,
height,
localModifications,
notLocalModifications,
}
return tip
}
const mouse_quadrant = e => ({
north: e.clientY > window.innerHeight / 2,
west: e.clientX > window.innerWidth / 2
})
const tip_position = (node, e, north, west) => ({
top: `${north
? e.pageY - node.clientHeight - 20
: e.pageY + 25}px`,
left: `${west
? e.pageX - node.clientWidth + 23
: e.pageX - 21}px`,
})
const handleBlur = ({target}) => {
if (target.hasAttribute && !target.hasAttribute('data-metatip') && state.tips.has(target))
wipe(state.tips.get(target))
}
const wipe = ({tip, e:{target}}) => {
tip.remove()
unobserve({tip, target})
state.tips.delete(target)
}
const togglePinned = e => {
const target = deepElementFromPoint(e.clientX, e.clientY)
if (e.altKey && !target.hasAttribute('data-metatip')) {
target.setAttribute('data-metatip', true)
state.tips.set(target, {
tip: state.active.tip,
e,
})
clearActive()
}
else if (target.hasAttribute('data-metatip')) {
target.removeAttribute('data-metatip')
wipe(state.tips.get(target))
}
}
const linkQueryClicked = ({detail:{ text, activator }}) => {
if (!text) return
queryPage('[data-pseudo-select]', el =>
el.removeAttribute('data-pseudo-select'))
queryPage(text + ':not([data-selected])', el =>
activator === 'mouseenter'
? el.setAttribute('data-pseudo-select', true)
: services.selectors.select(el))
}
const linkQueryHoverOut = e => {
queryPage('[data-pseudo-select]', el =>
el.removeAttribute('data-pseudo-select'))
}
const toggleTargetCursor = (key, target) =>
key
? target.setAttribute('data-pinhover', true)
: target.removeAttribute('data-pinhover')
const observe = ({tip, target}) => {
$(tip).on('query', linkQueryClicked)
$(tip).on('unquery', linkQueryHoverOut)
$(target).on('DOMNodeRemoved', handleBlur)
}
const unobserve = ({tip, target}) => {
$(tip).off('query', linkQueryClicked)
$(tip).off('unquery', linkQueryHoverOut)
$(target).off('DOMNodeRemoved', handleBlur)
}
const clearActive = () => {
state.active.tip = null
state.active.target = null
}