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
321 lines (248 loc) • 7.87 kB
JavaScript
import $ from 'blingblingjs'
import hotkeys from 'hotkeys-js'
import { getNodeIndex, showEdge, swapElements, notList } from '../utilities/'
import { toggleWatching } from './imageswap'
const key_events = 'up,down,left,right'
const state = {
drag: {
src: null,
parent: null,
parent_ui: [],
siblings: new Map(),
swapping: new Map(),
},
hover: {
dropzones: [],
observers: [],
},
}
// todo: indicator for when node can descend
// todo: have it work with shadowDOM
export function Moveable(visbug) {
hotkeys(key_events, (e, {key}) => {
if (e.cancelBubble) return
e.preventDefault()
e.stopPropagation()
visbug.selection().forEach(el => {
moveElement(el, key)
updateFeedback(el)
})
})
visbug.onSelectedUpdate(dragNDrop)
toggleWatching({watch: false})
return () => {
toggleWatching({watch: true})
visbug.removeSelectedCallback(dragNDrop)
clearListeners()
hotkeys.unbind(key_events)
}
}
export function moveElement(el, direction) {
if (!el) return
switch(direction) {
case 'left':
if (canMoveLeft(el))
el.parentNode.insertBefore(el, el.previousElementSibling)
else
showEdge(el.parentNode)
break
case 'right':
if (canMoveRight(el) && el.nextElementSibling.nextSibling)
el.parentNode.insertBefore(el, el.nextElementSibling.nextSibling)
else if (canMoveRight(el))
el.parentNode.appendChild(el)
else
showEdge(el.parentNode)
break
case 'up':
if (canMoveUp(el))
popOut({el})
break
case 'down':
if (canMoveUnder(el))
popOut({el, under: true})
else if (canMoveDown(el))
el.nextElementSibling.prepend(el)
break
}
}
export const canMoveLeft = el => el.previousElementSibling
export const canMoveRight = el => el.nextElementSibling
export const canMoveDown = el => el.nextElementSibling && el.nextElementSibling.children.length
export const canMoveUnder = el => !el.nextElementSibling && el.parentNode && el.parentNode.parentNode
export const canMoveUp = el => el.parentNode && el.parentNode.parentNode
export const popOut = ({el, under = false}) =>
el.parentNode.parentNode.insertBefore(el,
el.parentNode.parentNode.children[
under
? getNodeIndex(el) + 1
: getNodeIndex(el)])
export function dragNDrop(selection) {
if (!selection.length)
return
clearListeners()
const [src] = selection
const {parentNode} = src
const validMoveableChildren = [...parentNode.querySelectorAll(':scope > *' + notList)]
const tooManySelected = selection.length !== 1
const hasNoSiblingsToDrag = validMoveableChildren.length <= 1
const isAnSVG = src instanceof SVGElement
if (tooManySelected || hasNoSiblingsToDrag || isAnSVG)
return
validMoveableChildren.forEach(sibling =>
state.drag.siblings.set(sibling, createGripUI(sibling)))
state.drag.parent = parentNode
state.drag.parent_ui = createParentUI(parentNode)
moveWatch(state.drag.parent)
}
const moveWatch = node => {
const $node = $(node)
$node.on('mouseleave', dragDrop)
$node.on('dragstart', dragStart)
$node.on('drop', dragDrop)
state.drag.siblings.forEach((grip, sibling) => {
sibling.setAttribute('draggable', true)
$(sibling).on('dragover', dragOver)
$(sibling).on('mouseenter', siblingHoverIn)
$(sibling).on('mouseleave', siblingHoverOut)
})
}
const moveUnwatch = node => {
const $node = $(node)
$node.off('mouseleave', dragDrop)
$node.off('dragstart', dragStart)
$node.off('drop', dragDrop)
state.drag.siblings.forEach((grip, sibling) => {
sibling.removeAttribute('draggable')
$(sibling).off('dragover', dragOver)
$(sibling).off('mouseenter', siblingHoverIn)
$(sibling).off('mouseleave', siblingHoverOut)
})
}
const dragStart = ({target}) => {
if (!state.drag.siblings.has(target))
return
state.drag.src = target
state.hover.dropzones.push(createDropzoneUI(target))
state.drag.siblings.get(target).style.opacity = 0.01
target.setAttribute('visbug-drag-src', true)
ghostNode(target)
$('visbug-hover').forEach(el =>
!el.hasAttribute('visbug-drag-container') && el.remove())
}
const dragOver = e => {
if (
!state.drag.src ||
state.drag.swapping.get(e.target) ||
e.target.hasAttribute('visbug-drag-src') ||
!state.drag.siblings.has(e.currentTarget) ||
e.currentTarget !== e.target
) return
state.drag.swapping.set(e.target, true)
swapElements(state.drag.src, e.target)
setTimeout(() =>
state.drag.swapping.delete(e.target)
, 250)
}
const dragDrop = e => {
if (!state.drag.src) return
state.drag.src.removeAttribute('visbug-drag-src')
ghostBuster(state.drag.src)
if (state.drag.siblings.has(state.drag.src))
state.drag.siblings.get(state.drag.src).style.opacity = null
state.hover.dropzones.forEach(zone =>
zone.remove())
state.drag.src = null
}
const siblingHoverIn = ({target}) => {
if (!state.drag.siblings.has(target))
return
state.drag.siblings.get(target)
.toggleHovering({hovering:true})
}
const siblingHoverOut = ({target}) => {
if (!state.drag.siblings.has(target))
return
state.drag.siblings.get(target)
.toggleHovering({hovering:false})
}
const ghostNode = ({style}) => {
style.transition = 'opacity .25s ease-out'
style.opacity = 0.01
}
const ghostBuster = ({style}) => {
style.transition = null
style.opacity = null
}
const createDropzoneUI = el => {
const zone = document.createElement('visbug-corners')
zone.position = {el}
document.body.appendChild(zone)
const observer = new MutationObserver(list =>
zone.position = {el})
observer.observe(el.parentNode, {
childList: true,
subtree: true,
})
state.hover.observers.push(observer)
return zone
}
const createGripUI = el => {
const grip = document.createElement('visbug-grip')
grip.position = {el}
document.body.appendChild(grip)
const observer = new MutationObserver(list =>
grip.position = {el})
observer.observe(el.parentNode, {
childList: true,
subtree: true,
})
state.hover.observers.push(observer)
return grip
}
const createParentUI = parent => {
const hover = document.createElement('visbug-hover')
const label = document.createElement('visbug-label')
hover.position = {el:parent}
hover.setAttribute('visbug-drag-container', true)
label.text = 'Drag Bounds'
label.position = {boundingRect: parent.getBoundingClientRect()}
label.style.setProperty('--label-bg', 'var(--theme-purple)')
document.body.appendChild(hover)
document.body.appendChild(label)
const observer = new MutationObserver(list => {
hover.position = {el:parent}
label.position = {boundingRect: parent.getBoundingClientRect()}
})
observer.observe(parent, {
childList: true,
subtree: true,
})
state.hover.observers.push(observer)
return [hover,label]
}
export function clearListeners() {
moveUnwatch(state.drag.parent)
state.hover.observers.forEach(observer =>
observer.disconnect())
state.hover.dropzones.forEach(zone =>
zone.remove())
state.drag.siblings.forEach((grip, sibling) =>
grip.remove())
state.drag.parent_ui.forEach(ui =>
ui.remove())
state.hover.observers = []
state.hover.dropzones = []
state.drag.parent_ui = []
state.drag.siblings.clear()
}
const updateFeedback = el => {
let options = ''
// get current elements offset/size
if (canMoveLeft(el)) options += '⇠'
if (canMoveRight(el)) options += '⇢'
if (canMoveDown(el)) options += '⇣'
if (canMoveUp(el)) options += '⇡'
// create/move arrows in absolute/fixed to overlay element
options && console.info('%c'+options, "font-size: 2rem;")
}