Initial work on drag-and-drop

This commit is contained in:
Magnus Åhall 2026-06-14 14:36:28 +02:00
parent 1055404dc0
commit 61b0ba9ada
10 changed files with 514 additions and 8 deletions

View file

@ -449,15 +449,20 @@ export class N2Sidebar extends CustomHTMLElement {
treenode?.scrollIntoView({ block: 'nearest' })
}// }}}
}
customElements.define('n2-sidebar', N2Sidebar)
export class N2TreeNode extends CustomHTMLElement {
static DRAG_ICON = new Image()
static DRAG_ICON_OK = new Image()
static {// {{{
N2TreeNode.DRAG_ICON.src = `/images/${_VERSION}/leaf.svg`
N2TreeNode.DRAG_ICON_OK.src = `/images/${_VERSION}/expanded.svg`
this.tmpl = document.createElement('template')
this.tmpl.innerHTML = `
<style>
n2-sidebar:focus-within {
.el-name {
& > .el-name {
&.selected {
span {
position:relative;
@ -478,10 +483,60 @@ export class N2TreeNode extends CustomHTMLElement {
}
}
}
n2-treenode {
& > .el-name {
white-space: nowrap;
width: min-content;
}
&.drag-source {
& > .el-name {
position: relative;
}
& > .el-name:after {
position: absolute;
content: url('/images/${_VERSION}/icon_drag_source.svg');
filter: var(--colorize);
top: -1px;
right: -24px;
}
}
&.drag-target {
position: relative;
& > .el-name {
anchor-name: --name;
}
& > .el-name:after {
content: '';
position: absolute;
border: 2px dashed #888;
top: calc(anchor(--name top) - 12px);
right: calc(anchor(--name right) - 8px);
bottom: calc(anchor(--name bottom) - 8px);
left: calc(anchor(--name left) - 40px);
pointer-events: none;
}
& > .el-drag-icon {
display: block;
top: 0px;
left: 0px;
z-index: 16384;
}
}
}
</style>
<div data-el="expand-toggle" class="expand-toggle">
<img data-el="expand">
<img data-el="expand" draggable="false">
</div>
<div data-el="name" class="name"><span></span></div>
<div data-el="children" class="children"></div>
@ -490,6 +545,7 @@ export class N2TreeNode extends CustomHTMLElement {
constructor(sidebar, node, parent) {//{{{
super()
this.setAttribute('draggable', 'true')
this.classList.add('node')
this.sidebar = sidebar
@ -498,6 +554,7 @@ export class N2TreeNode extends CustomHTMLElement {
this.children_populated = false
this.rendered = false
this.dragNode = null
this.elExpandToggle.addEventListener('click', () => this.sidebar.setNodeExpanded(this.node, !this.sidebar.getNodeExpanded(this.node.UUID)))
this.elName.addEventListener('click', () => _mbus.dispatch('TREE_NODE_SELECTED', this.node))
@ -505,6 +562,70 @@ export class N2TreeNode extends CustomHTMLElement {
_mbus.subscribe(`NODE_EXPAND_${node.UUID}`, _state => {
this.render(true)
})
// Drag-and-dropping of nodes
this.addEventListener('dragstart', event => this.dragStart(event))
this.addEventListener('dragend', event => this.dragEnd(event))
this.addEventListener('dragover', event => this.dragOver(event))
this.addEventListener('drop', event => this.dragDrop(event))
this.elName.addEventListener('dragenter', event => this.dragEnter(event))
this.elName.addEventListener('dragleave', event => this.dragLeave(event))
}// }}}
dragStart(e) {// {{{
if (this.node.isModified()) {
alert('Save note before moving it.')
e.stopPropagation()
e.preventDefault()
return
}
this.classList.add('drag-source')
const blankPixel = new Image()
blankPixel.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
e.dataTransfer.setDragImage(blankPixel, 0, 0)
e.dataTransfer.allowedEffects = 'none'
e.stopPropagation()
_app.dragIcon.start()
}// }}}
dragEnd(e) {// {{{
this.classList.remove('drag-source')
_app.dragIcon.end()
e.stopPropagation()
}// }}}
dragOver(e) {// {{{
e.dataTransfer.dropEffect = 'move'
e.preventDefault()
}// }}}
async dragDrop(e) {// {{{
e.stopPropagation()
const moveToNode = _app.dragIcon.getTarget()
await _app.moveNode(this.node, moveToNode.node.UUID)
return
_app.sidebar.setNodeExpanded(moveToNode, true)
await this.render(true, true)
await moveToNode.render(true, true)
this.dragLeave(e)
}// }}}
dragEnter(e) {// {{{
const targetNode = e.target.closest('n2-treenode')
if (targetNode.classList.contains('drag-source'))
return
e.stopPropagation()
_app.dragIcon.icon('ok')
this.classList.add('drag-target')
_app.dragIcon.setTarget(this)
}// }}}
dragLeave(e) {// {{{
e.stopPropagation()
e.dataTransfer.dropEffect = 'none'
e.dataTransfer.setDragImage(N2TreeNode.DRAG_ICON, -16, 8)
_app.dragIcon.icon('')
this.classList.remove('drag-target')
_app.dragIcon.setTarget(null)
}// }}}
async fetchChildren(force_fetch) {//{{{
if (this.children_populated && !force_fetch)
@ -575,6 +696,8 @@ export class N2TreeNode extends CustomHTMLElement {
img.setAttribute('src', newSrc)
}// }}}
}
customElements.define('n2-sidebar', N2Sidebar)
customElements.define('n2-treenode', N2TreeNode)
// vim: foldmethod=marker