Dragonwatch
Dragonwatch is built around two functions, drag and watch, which is where the name comes from. Its event model also makes the same interactions easy to replay remotely.
SwapSet 1
SwapSet 2
DragTarget with replace mode
DragTarget 1
dragged
replace A
replace B
DragTarget 2
dragged
replace A
replace B
DragTarget with append mode
DragTarget 1
dragged
append A
append B
DragTarget 2
dragged
append A
append B
Code
import { SwapSet, DragTarget } from '@sovereignbase/dragonwatch'
const controlsArr: HTMLElement[] = Array.from(
document.querySelectorAll('[data-swap-set]')
)
const swapSets: SwapSet[] = []
for (const controls of controlsArr) {
if (!controls) throw new Error()
for (let i = 0; i < 9; i++) {
const box = document.createElement('div')
box.id = `box:${i + 1}`
box.textContent = `${i + 1}`
void controls.appendChild(box)
}
const swapSet = new SwapSet(controls.children)
void swapSets.push(swapSet)
swapSet.addEventListener('drag', ({ detail }) => {
for (const otherSet of swapSets) {
if (otherSet === swapSet) continue
const thisEl = otherSet.getMemberById(detail.thisEl.id)
if (!thisEl) continue
void otherSet.remoteDrag({ thisEl, x: detail.x, y: detail.y })
}
})
swapSet.addEventListener('swap', ({ detail }) => {
for (const otherSet of swapSets) {
if (otherSet === swapSet) continue
const thisEl = otherSet.getMemberById(detail.thisEl.id)
const withEl = otherSet.getMemberById(detail.withEl.id)
if (!thisEl || !withEl) continue
void otherSet.remoteSwap({ thisEl, withEl })
}
})
swapSet.addEventListener('settle', ({ detail }) => {
for (const otherSet of swapSets) {
if (otherSet === swapSet) continue
const thisEl = otherSet.getMemberById(detail.thisEl.id)
if (!thisEl) continue
void otherSet.remoteSettle({ thisEl })
}
})
}
const connect = (
demo: HTMLElement,
template: HTMLTemplateElement,
targetFor: (
dragged: HTMLElement,
targets: readonly HTMLElement[]
) => DragTarget
): void => {
const pair: HTMLElement | null = demo.querySelector('[data-target-pair]')
const reset: HTMLButtonElement | null = demo.querySelector('[data-reset]')
if (!pair || !reset) throw new Error()
let dragTargets: DragTarget[] = []
const fill = (): void => {
pair.replaceChildren(template.content.cloneNode(true))
dragTargets = []
const rows = Array.from(pair.querySelectorAll('.target-row'))
for (const row of rows) {
const dragged: HTMLElement | null = row.querySelector('[data-dragged]')
const targets = Array.from(row.querySelectorAll('[data-target]')).filter(
(element): element is HTMLElement => element instanceof HTMLElement
)
if (!dragged || targets.length === 0) throw new Error()
dragged.id = `${demo.dataset.targetDemo}:dragged`
for (const [index, target] of targets.entries())
target.id = `${demo.dataset.targetDemo}:target:${index}`
void dragTargets.push(targetFor(dragged, targets))
}
for (const dragTarget of dragTargets) watchTarget(dragTarget, dragTargets)
}
reset.addEventListener('click', fill)
fill()
}
const watchTarget = (
dragTarget: DragTarget,
dragTargets: readonly DragTarget[]
): void => {
dragTarget.addEventListener('intersecting', ({ detail }) => {
detail.withEl.dataset.intersecting = 'true'
for (const otherTarget of dragTargets) {
if (otherTarget === dragTarget) continue
const withEl = otherTarget.getTargetById(detail.withEl.id)
if (withEl) withEl.dataset.intersecting = 'true'
}
})
dragTarget.addEventListener('notintersecting', ({ detail }) => {
delete detail.withEl.dataset.intersecting
for (const otherTarget of dragTargets) {
if (otherTarget === dragTarget) continue
const withEl = otherTarget.getTargetById(detail.withEl.id)
if (withEl) delete withEl.dataset.intersecting
}
})
dragTarget.addEventListener('drag', ({ detail }) => {
for (const otherTarget of dragTargets) {
if (otherTarget === dragTarget) continue
if (otherTarget.dragged.id !== detail.thisEl.id) continue
void otherTarget.remoteDrag({
thisEl: otherTarget.dragged,
x: detail.x,
y: detail.y,
})
}
})
dragTarget.addEventListener('swap', ({ detail }) => {
delete detail.withEl.dataset.intersecting
for (const otherTarget of dragTargets) {
if (otherTarget === dragTarget) continue
if (otherTarget.dragged.id !== detail.thisEl.id) continue
const withEl = otherTarget.getTargetById(detail.withEl.id)
if (!withEl) continue
delete withEl.dataset.intersecting
void otherTarget.remoteSwap({ thisEl: otherTarget.dragged, withEl })
}
})
dragTarget.addEventListener('settle', ({ detail }) => {
for (const target of dragTarget.targets) delete target.dataset.intersecting
for (const otherTarget of dragTargets) {
if (otherTarget === dragTarget) continue
if (otherTarget.dragged.id !== detail.thisEl.id) continue
for (const target of otherTarget.targets)
delete target.dataset.intersecting
void otherTarget.remoteSettle({ thisEl: otherTarget.dragged })
}
})
}
const replaceDemo: HTMLElement | null = document.querySelector(
'[data-replace-demo]'
)
const appendDemo: HTMLElement | null =
document.querySelector('[data-append-demo]')
const replaceTemplate: HTMLTemplateElement | null = document.querySelector(
'#replace-demo-template'
)
const appendTemplate: HTMLTemplateElement | null = document.querySelector(
'#append-demo-template'
)
if (!replaceDemo || !appendDemo || !replaceTemplate || !appendTemplate)
throw new Error()
connect(
replaceDemo,
replaceTemplate,
(dragged, targets) => new DragTarget(dragged, targets, 'replace')
)
connect(
appendDemo,
appendTemplate,
(dragged, targets) => new DragTarget(dragged, targets, 'append')
)