Initial work on drag-and-drop
This commit is contained in:
parent
1055404dc0
commit
61b0ba9ada
10 changed files with 514 additions and 8 deletions
|
|
@ -12,6 +12,7 @@ export class App {
|
|||
this.crumbs = new N2Crumbs()
|
||||
this.crumbsElement = document.getElementById('crumbs')
|
||||
this.nodeUI = document.getElementById('note')
|
||||
this.dragIcon = new N2DragIcon()
|
||||
|
||||
this.sidebar.render().then(sidebar => {
|
||||
document.getElementById('tree').append(sidebar)
|
||||
|
|
@ -68,6 +69,7 @@ export class App {
|
|||
})
|
||||
|
||||
document.querySelector('#page-root .create').addEventListener('click', () => this.createNode())
|
||||
document.body.append(this.dragIcon)
|
||||
|
||||
_mbus.dispatch('SHOW_PAGE', { page: 'node' })
|
||||
|
||||
|
|
@ -78,7 +80,6 @@ export class App {
|
|||
// There a slight delay to initiate sync seems reasonable.
|
||||
setTimeout(() => window._sync.run(), 1000)
|
||||
}// }}}
|
||||
|
||||
keyHandler(event) {//{{{
|
||||
let handled = true
|
||||
|
||||
|
|
@ -151,6 +152,10 @@ export class App {
|
|||
async saveNode() {//{{{
|
||||
|
||||
}//}}}
|
||||
async moveNode(node, targetNodeUUID) {// {{{
|
||||
node.moveToParent(targetNodeUUID)
|
||||
await node.save()
|
||||
}// }}}
|
||||
async createNode(createUnderUUID) {//{{{
|
||||
const parentUUID = createUnderUUID ? createUnderUUID : this.currentNode.UUID
|
||||
const p = createUnderUUID ? 'Name for sibling document' : 'Name for sub-document'
|
||||
|
|
@ -239,7 +244,6 @@ class N2Crumbs extends CustomHTMLElement {
|
|||
return this
|
||||
}// }}}
|
||||
}
|
||||
customElements.define('n2-crumbs', N2Crumbs)
|
||||
|
||||
class N2Crumb extends CustomHTMLElement {
|
||||
static {// {{{
|
||||
|
|
@ -270,7 +274,6 @@ class N2Crumb extends CustomHTMLElement {
|
|||
this.elLink.addEventListener('click', () => _mbus.dispatch("GO_TO_NODE", { nodeUUID: this.uuid, dontPush: false, dontExpand: true }))
|
||||
}// }}}
|
||||
}
|
||||
customElements.define('n2-crumb', N2Crumb)
|
||||
|
||||
function tmpl(html) {// {{{
|
||||
const el = document.createElement('template')
|
||||
|
|
@ -344,4 +347,52 @@ class OpSearch extends Op {
|
|||
}// }}}
|
||||
}
|
||||
|
||||
class N2DragIcon extends CustomHTMLElement {
|
||||
static {// {{{
|
||||
this.tmpl = document.createElement('template')
|
||||
this.tmpl.innerHTML = `
|
||||
<style>
|
||||
:host {
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index: 16384;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
<img data-el="icon" src="/images/${_VERSION}/icon_drag.svg">
|
||||
`
|
||||
}// }}}
|
||||
constructor() {// {{{
|
||||
super(true)
|
||||
|
||||
document.addEventListener('dragover', e => {
|
||||
this.style.left = `${e.clientX + 8}px`
|
||||
this.style.top = `${e.clientY}px`
|
||||
})
|
||||
|
||||
this.dragTarget = null
|
||||
}// }}}
|
||||
start() {// {{{
|
||||
this.style.display = 'block'
|
||||
}// }}}
|
||||
end() {// {{{
|
||||
this.style.display = 'none'
|
||||
}// }}}
|
||||
icon(name) {// {{{
|
||||
if (name != '')
|
||||
name = '_' + name
|
||||
this.elIcon.setAttribute('src', `/images/${_VERSION}/icon_drag${name}.svg`)
|
||||
}// }}}
|
||||
setTarget(t) {// {{{
|
||||
this.dragTarget = t
|
||||
}// }}}
|
||||
getTarget() {// {{{
|
||||
return this.dragTarget
|
||||
}// }}}
|
||||
}
|
||||
|
||||
customElements.define('n2-crumbs', N2Crumbs)
|
||||
customElements.define('n2-crumb', N2Crumb)
|
||||
customElements.define('n2-dragicon', N2DragIcon)
|
||||
|
||||
// vim: foldmethod=marker
|
||||
|
|
|
|||
|
|
@ -247,6 +247,7 @@ export class NodeStore {
|
|||
nodeStore = t.objectStore('nodes')
|
||||
|
||||
t.oncomplete = (_event) => {
|
||||
console.log('complete')
|
||||
resolve()
|
||||
}
|
||||
|
||||
|
|
@ -358,6 +359,7 @@ class SimpleNodeStore {
|
|||
// Node to be moved is first stored in the new queue.
|
||||
const req = store.put(node.data)
|
||||
req.onsuccess = () => {
|
||||
console.log('here')
|
||||
resolve()
|
||||
}
|
||||
req.onerror = (event) => {
|
||||
|
|
|
|||
|
|
@ -422,6 +422,11 @@ export class Node {
|
|||
getParent() {//{{{
|
||||
return this._parent
|
||||
}//}}}
|
||||
moveToParent(newParentUUID) {// {{{
|
||||
this.ParentUUID = newParentUUID
|
||||
this.data.ParentUUID = newParentUUID
|
||||
this._modified = true
|
||||
}// }}}
|
||||
isLastSibling() {//{{{
|
||||
return this._sibling_after === null
|
||||
}//}}}
|
||||
|
|
@ -463,9 +468,10 @@ export class Node {
|
|||
|
||||
// When stored into database and ancestry was changed,
|
||||
// the ancestry path could be interesting.
|
||||
/*
|
||||
const ancestors = await nodeStore.getNodeAncestry(this)
|
||||
this.data.Ancestors = ancestors.map(a => a.get('Name')).reverse()
|
||||
|
||||
*/
|
||||
/* The node history is a local store for node history.
|
||||
* This could be provisioned from the server or cleared if
|
||||
* deemed unnecessary.
|
||||
|
|
@ -481,12 +487,17 @@ export class Node {
|
|||
const history = nodeStore.nodesHistory.add(this)
|
||||
|
||||
// Updated node is added to the send queue to be stored on server.
|
||||
|
||||
const sendQueue = nodeStore.sendQueue.add(this)
|
||||
|
||||
// Updated node is saved to the primary node store.
|
||||
const nodeStoreAdding = nodeStore.add([this])
|
||||
|
||||
return Promise.all([history, sendQueue, nodeStoreAdding])
|
||||
console.log('waiting')
|
||||
await Promise.all([history, sendQueue, nodeStoreAdding])
|
||||
console.log('waiting done')
|
||||
|
||||
return
|
||||
}//}}}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue