Initial work on drag-and-drop
This commit is contained in:
parent
1055404dc0
commit
61b0ba9ada
10 changed files with 514 additions and 8 deletions
119
sql/00006.sql
Normal file
119
sql/00006.sql
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
CREATE OR REPLACE PROCEDURE public.add_nodes(IN p_user_id integer, IN p_client_uuid uuid, IN p_nodes jsonb)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $procedure$
|
||||||
|
|
||||||
|
DECLARE
|
||||||
|
node_data jsonb;
|
||||||
|
node_updated timestamptz;
|
||||||
|
db_updated timestamptz;
|
||||||
|
db_uuid uuid;
|
||||||
|
db_client uuid;
|
||||||
|
db_history_uuid uuid;
|
||||||
|
node_uuid uuid;
|
||||||
|
node_parent_uuid uuid;
|
||||||
|
node_history_uuid uuid;
|
||||||
|
|
||||||
|
BEGIN
|
||||||
|
FOR node_data IN SELECT * FROM jsonb_array_elements(p_nodes)
|
||||||
|
LOOP
|
||||||
|
node_uuid = (node_data->>'UUID')::uuid;
|
||||||
|
node_history_uuid = (node_data->>'HistoryUUID')::uuid;
|
||||||
|
node_updated = (node_data->>'Updated')::timestamptz;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- Frontend is using an all-zero UUID to define the root node.
|
||||||
|
-- Database is using NULL.
|
||||||
|
IF node_data->>'ParentUUID' = '00000000-0000-0000-0000-000000000000' OR node_data->>'ParentUUID' = '' THEN
|
||||||
|
node_parent_uuid = NULL;
|
||||||
|
ELSE
|
||||||
|
node_parent_uuid = (node_data->>'ParentUUID')::uuid;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- Every jode has a new history UUID to keep the history entry uniquely identifiable
|
||||||
|
-- across clients. A history entry could potentially be sent again, but should be
|
||||||
|
-- safe to ignore as every change to a node should have a new history UUID.
|
||||||
|
--
|
||||||
|
-- The current node is also stored as history.
|
||||||
|
INSERT INTO node_history(
|
||||||
|
user_id, "uuid", "history_uuid", parents, created, updated,
|
||||||
|
"name", "content", "content_encrypted",
|
||||||
|
client
|
||||||
|
)
|
||||||
|
VALUES(
|
||||||
|
p_user_id, -- combined key
|
||||||
|
node_uuid, -- combined key
|
||||||
|
node_history_uuid, -- combined key
|
||||||
|
(jsonb_populate_record(null::json_ancestor_array, node_data))."Ancestors",
|
||||||
|
COALESCE((node_data->>'Created')::timestamptz, NOW()),
|
||||||
|
COALESCE((node_data->>'Updated')::timestamptz, NOW()),
|
||||||
|
(node_data->>'Name')::varchar,
|
||||||
|
(node_data->>'Content')::text,
|
||||||
|
'', /* content_encrypted */
|
||||||
|
p_client_uuid
|
||||||
|
)
|
||||||
|
ON CONFLICT ("user_id", "uuid", "history_uuid")
|
||||||
|
DO NOTHING;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- Retrieve the current modified timestamp for this node from the database.
|
||||||
|
SELECT
|
||||||
|
uuid, updated, client
|
||||||
|
INTO
|
||||||
|
db_uuid, db_updated, db_client
|
||||||
|
FROM public."node"
|
||||||
|
WHERE
|
||||||
|
user_id = p_user_id AND
|
||||||
|
uuid::uuid = node_uuid::uuid;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- Is the node not in database? It needs to be created.
|
||||||
|
IF db_uuid IS NULL THEN
|
||||||
|
RAISE NOTICE '01 New node %', node_uuid;
|
||||||
|
|
||||||
|
INSERT INTO public."node" (
|
||||||
|
user_id, "uuid", parent_uuid, created, updated,
|
||||||
|
"name", "content", "content_encrypted",
|
||||||
|
client
|
||||||
|
)
|
||||||
|
VALUES(
|
||||||
|
p_user_id,
|
||||||
|
node_uuid,
|
||||||
|
node_parent_uuid,
|
||||||
|
COALESCE((node_data->>'Created')::timestamptz, NOW()),
|
||||||
|
COALESCE((node_data->>'Updated')::timestamptz, NOW()),
|
||||||
|
(node_data->>'Name')::varchar,
|
||||||
|
(node_data->>'Content')::text,
|
||||||
|
'', /* content_encrypted */
|
||||||
|
p_client_uuid
|
||||||
|
);
|
||||||
|
|
||||||
|
CONTINUE;
|
||||||
|
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- Update the public node as well if it was older than incoming node.
|
||||||
|
IF node_updated > db_updated THEN
|
||||||
|
UPDATE public."node"
|
||||||
|
SET
|
||||||
|
updated = (node_data->>'Updated')::timestamptz,
|
||||||
|
updated_seq = nextval('node_updates'),
|
||||||
|
parent_uuid = (node_data->>'ParentUUID')::uuid,
|
||||||
|
name = (node_data->>'Name')::varchar,
|
||||||
|
content = (node_data->>'Content')::text,
|
||||||
|
client = p_client_uuid
|
||||||
|
WHERE
|
||||||
|
user_id = p_user_id AND
|
||||||
|
uuid::uuid = node_uuid::uuid;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
END LOOP;
|
||||||
|
END
|
||||||
|
$procedure$
|
||||||
|
;
|
||||||
|
|
@ -418,6 +418,8 @@ n2-nodeui {
|
||||||
font-size: 1.75em;
|
font-size: 1.75em;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: min-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-functions {
|
.el-functions {
|
||||||
|
|
|
||||||
71
static/images/icon_drag.svg
Normal file
71
static/images/icon_drag.svg
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 6.35 6.35"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="icon_drag.svg"
|
||||||
|
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||||
|
xml:space="preserve"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:zoom="22.627417"
|
||||||
|
inkscape:cx="11.291611"
|
||||||
|
inkscape:cy="10.84967"
|
||||||
|
inkscape:window-width="2190"
|
||||||
|
inkscape:window-height="1401"
|
||||||
|
inkscape:window-x="1463"
|
||||||
|
inkscape:window-y="18"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false" /><defs
|
||||||
|
id="defs1" /><g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(-107.95,-148.16667)"><title
|
||||||
|
id="title1">folder-open</title><title
|
||||||
|
id="title1-1">folder-open-outline</title><title
|
||||||
|
id="title1-5">notebook-outline</title><title
|
||||||
|
id="title1-8">text-box-outline</title><path
|
||||||
|
style="fill:#ffffff;stroke-width:0.264583"
|
||||||
|
d="m 108.3015,148.56838 h 3.95851 v 3.96688 h -3.95851 z"
|
||||||
|
id="path3"
|
||||||
|
sodipodi:nodetypes="ccccc" /><path
|
||||||
|
d="m 108.47917,148.16667 c -0.29369,0 -0.52917,0.23548 -0.52917,0.52917 V 152.4 c 0,0.29369 0.23548,0.52917 0.52917,0.52917 h 3.70416 c 0.29369,0 0.52917,-0.23548 0.52917,-0.52917 v -3.70416 c 0,-0.29369 -0.23548,-0.52917 -0.52917,-0.52917 h -3.70416 m 0,0.52917 h 3.70416 V 152.4 h -3.70416 v -3.70416"
|
||||||
|
id="path1"
|
||||||
|
style="fill:#666666;fill-opacity:1;stroke:none;stroke-width:0.264583"
|
||||||
|
sodipodi:nodetypes="cssssssscccccc" /><path
|
||||||
|
d="m 109.00833,149.225 v 0.52917 h 2.64584 V 149.225 h -2.64584 m 0,1.05834 v 0.52916 h 2.64584 v -0.52916 h -2.64584 m 0,1.05833 v 0.52917 h 2.64584 v -0.52917 z"
|
||||||
|
id="path2"
|
||||||
|
style="fill:#999999;fill-opacity:1;stroke-width:0.264583"
|
||||||
|
sodipodi:nodetypes="ccccccccccccccc" /><g
|
||||||
|
id="g5"
|
||||||
|
transform="translate(0.26458031,0.26458956)"><g
|
||||||
|
id="g8"
|
||||||
|
transform="matrix(1.2067669,0,0,1.2067669,-23.043599,-31.373186)"><circle
|
||||||
|
style="fill:#800000;fill-opacity:1;stroke:none;stroke-width:1.14487;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path8"
|
||||||
|
cx="112.05721"
|
||||||
|
cy="152.28557"
|
||||||
|
r="1.5347482" /></g><path
|
||||||
|
style="fill:none;stroke:#ffffff;stroke-width:0.79375;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 111.32748,151.54414 1.71172,1.71172"
|
||||||
|
id="path4" /><path
|
||||||
|
style="fill:none;stroke:#ffffff;stroke-width:0.79375;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 113.0392,151.54414 -1.71172,1.71172"
|
||||||
|
id="path5" /></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.1 KiB |
75
static/images/icon_drag_ok.svg
Normal file
75
static/images/icon_drag_ok.svg
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 6.35 6.35"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="icon_drag_ok.svg"
|
||||||
|
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||||
|
xml:space="preserve"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:zoom="32"
|
||||||
|
inkscape:cx="13.859375"
|
||||||
|
inkscape:cy="14.890625"
|
||||||
|
inkscape:window-width="2190"
|
||||||
|
inkscape:window-height="1401"
|
||||||
|
inkscape:window-x="1463"
|
||||||
|
inkscape:window-y="18"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false" /><defs
|
||||||
|
id="defs1" /><g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(-107.95,-148.16667)"><title
|
||||||
|
id="title1">folder-open</title><title
|
||||||
|
id="title1-1">folder-open-outline</title><title
|
||||||
|
id="title1-5">notebook-outline</title><title
|
||||||
|
id="title1-8">text-box-outline</title><path
|
||||||
|
style="fill:#ffffff;stroke-width:0.264583"
|
||||||
|
d="m 108.3015,148.56838 h 3.95851 v 3.96688 h -3.95851 z"
|
||||||
|
id="path3"
|
||||||
|
sodipodi:nodetypes="ccccc" /><title
|
||||||
|
id="title1-53">text-box-check-outline</title><path
|
||||||
|
style="stroke-width:0.264583;fill:#999999;fill-opacity:1"
|
||||||
|
d="m 111.65417,149.75417 h -2.64584 V 149.225 h 2.64584"
|
||||||
|
id="path7" /><path
|
||||||
|
style="fill:#999999;fill-opacity:1;stroke-width:0.264583"
|
||||||
|
d="m 109.00833,150.8125 v -0.52916 h 2.64584 v 0.52916"
|
||||||
|
id="path6"
|
||||||
|
sodipodi:nodetypes="cccc" /><path
|
||||||
|
style="fill:#999999;fill-opacity:1;stroke-width:0.313059"
|
||||||
|
d="m 111.65417,151.87084 h -2.64584 v -0.52917 h 2.64584"
|
||||||
|
id="path5"
|
||||||
|
sodipodi:nodetypes="cccc" /><path
|
||||||
|
style="fill:#666666;fill-opacity:1;stroke-width:0.264583"
|
||||||
|
d="m 111.26226,152.92917 h -2.78309 c -0.29369,0 -0.52917,-0.23548 -0.52917,-0.52917 v -3.70416 c 0,-0.29369 0.23548,-0.52917 0.52917,-0.52917 h 3.70416 c 0.29369,0 0.52917,0.23548 0.52917,0.52917 v 2.7004 c -0.1614,-0.0926 -0.33867,-0.15875 -0.52917,-0.1905 v -2.5099 h -3.70416 V 152.4 h 2.59259 c 0.0318,0.1905 0.0979,0.36777 0.1905,0.52917"
|
||||||
|
id="path4"
|
||||||
|
sodipodi:nodetypes="cssssssccccccc" /><g
|
||||||
|
id="g8"
|
||||||
|
transform="matrix(1.2067669,0,0,1.2067669,-23.043599,-31.373186)"><ellipse
|
||||||
|
style="fill:#338000;fill-opacity:1;stroke:none;stroke-width:1.14488;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path8"
|
||||||
|
cx="112.27646"
|
||||||
|
cy="152.50487"
|
||||||
|
rx="1.5347482"
|
||||||
|
ry="1.5347837" /><path
|
||||||
|
style="fill:#ffffff;fill-opacity:1;stroke-width:0.264583"
|
||||||
|
d="m 112.01188,153.31978 -0.72761,-0.79375 0.30692,-0.30692 0.42069,0.42069 0.94985,-0.94985 0.30692,0.37306"
|
||||||
|
id="path1-5" /></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.2 KiB |
49
static/images/icon_drag_source.svg
Normal file
49
static/images/icon_drag_source.svg
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="15.999988"
|
||||||
|
height="15.999988"
|
||||||
|
viewBox="0 0 4.2333301 4.2333301"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="icon_drag_source.svg"
|
||||||
|
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:zoom="8.4025252"
|
||||||
|
inkscape:cx="45.522029"
|
||||||
|
inkscape:cy="-1.1306125"
|
||||||
|
inkscape:window-width="2190"
|
||||||
|
inkscape:window-height="1401"
|
||||||
|
inkscape:window-x="1463"
|
||||||
|
inkscape:window-y="18"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(-187.14773,-188.73523)">
|
||||||
|
<title
|
||||||
|
id="title1">drag-variant</title>
|
||||||
|
<path
|
||||||
|
d="m 191.38106,190.85189 -0.8907,0.89269 -0.49793,-0.49594 0.39279,-0.39675 -0.39279,-0.38881 0.49793,-0.49792 0.8907,0.88673 m -2.11667,-2.11666 0.88674,0.89071 -0.49791,0.49792 -0.38883,-0.39278 -0.39674,0.39278 -0.49594,-0.49792 0.89268,-0.89071 m 0,4.23333 -0.88673,-0.8907 0.49792,-0.49793 0.38881,0.39279 0.39675,-0.39279 0.49594,0.49793 -0.89269,0.8907 m -2.11666,-2.11667 0.89071,-0.89268 0.49792,0.49594 -0.39278,0.39674 0.39278,0.38883 -0.49792,0.49791 -0.89071,-0.88674 m 2.11666,-0.39674 a 0.3967509,0.3967509 0 0 1 0.39675,0.39674 0.3967509,0.3967509 0 0 1 -0.39675,0.39675 0.3967509,0.3967509 0 0 1 -0.39674,-0.39675 0.3967509,0.3967509 0 0 1 0.39674,-0.39674 z"
|
||||||
|
id="path1"
|
||||||
|
style="stroke-width:0.198375" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
|
|
@ -12,6 +12,7 @@ export class App {
|
||||||
this.crumbs = new N2Crumbs()
|
this.crumbs = new N2Crumbs()
|
||||||
this.crumbsElement = document.getElementById('crumbs')
|
this.crumbsElement = document.getElementById('crumbs')
|
||||||
this.nodeUI = document.getElementById('note')
|
this.nodeUI = document.getElementById('note')
|
||||||
|
this.dragIcon = new N2DragIcon()
|
||||||
|
|
||||||
this.sidebar.render().then(sidebar => {
|
this.sidebar.render().then(sidebar => {
|
||||||
document.getElementById('tree').append(sidebar)
|
document.getElementById('tree').append(sidebar)
|
||||||
|
|
@ -68,6 +69,7 @@ export class App {
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector('#page-root .create').addEventListener('click', () => this.createNode())
|
document.querySelector('#page-root .create').addEventListener('click', () => this.createNode())
|
||||||
|
document.body.append(this.dragIcon)
|
||||||
|
|
||||||
_mbus.dispatch('SHOW_PAGE', { page: 'node' })
|
_mbus.dispatch('SHOW_PAGE', { page: 'node' })
|
||||||
|
|
||||||
|
|
@ -78,7 +80,6 @@ export class App {
|
||||||
// There a slight delay to initiate sync seems reasonable.
|
// There a slight delay to initiate sync seems reasonable.
|
||||||
setTimeout(() => window._sync.run(), 1000)
|
setTimeout(() => window._sync.run(), 1000)
|
||||||
}// }}}
|
}// }}}
|
||||||
|
|
||||||
keyHandler(event) {//{{{
|
keyHandler(event) {//{{{
|
||||||
let handled = true
|
let handled = true
|
||||||
|
|
||||||
|
|
@ -150,6 +151,10 @@ export class App {
|
||||||
}//}}}
|
}//}}}
|
||||||
async saveNode() {//{{{
|
async saveNode() {//{{{
|
||||||
|
|
||||||
|
}//}}}
|
||||||
|
async moveNode(node, targetNodeUUID) {// {{{
|
||||||
|
node.moveToParent(targetNodeUUID)
|
||||||
|
await node.save()
|
||||||
}// }}}
|
}// }}}
|
||||||
async createNode(createUnderUUID) {//{{{
|
async createNode(createUnderUUID) {//{{{
|
||||||
const parentUUID = createUnderUUID ? createUnderUUID : this.currentNode.UUID
|
const parentUUID = createUnderUUID ? createUnderUUID : this.currentNode.UUID
|
||||||
|
|
@ -239,7 +244,6 @@ class N2Crumbs extends CustomHTMLElement {
|
||||||
return this
|
return this
|
||||||
}// }}}
|
}// }}}
|
||||||
}
|
}
|
||||||
customElements.define('n2-crumbs', N2Crumbs)
|
|
||||||
|
|
||||||
class N2Crumb extends CustomHTMLElement {
|
class N2Crumb extends CustomHTMLElement {
|
||||||
static {// {{{
|
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 }))
|
this.elLink.addEventListener('click', () => _mbus.dispatch("GO_TO_NODE", { nodeUUID: this.uuid, dontPush: false, dontExpand: true }))
|
||||||
}// }}}
|
}// }}}
|
||||||
}
|
}
|
||||||
customElements.define('n2-crumb', N2Crumb)
|
|
||||||
|
|
||||||
function tmpl(html) {// {{{
|
function tmpl(html) {// {{{
|
||||||
const el = document.createElement('template')
|
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
|
// vim: foldmethod=marker
|
||||||
|
|
|
||||||
|
|
@ -247,6 +247,7 @@ export class NodeStore {
|
||||||
nodeStore = t.objectStore('nodes')
|
nodeStore = t.objectStore('nodes')
|
||||||
|
|
||||||
t.oncomplete = (_event) => {
|
t.oncomplete = (_event) => {
|
||||||
|
console.log('complete')
|
||||||
resolve()
|
resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -358,6 +359,7 @@ class SimpleNodeStore {
|
||||||
// Node to be moved is first stored in the new queue.
|
// Node to be moved is first stored in the new queue.
|
||||||
const req = store.put(node.data)
|
const req = store.put(node.data)
|
||||||
req.onsuccess = () => {
|
req.onsuccess = () => {
|
||||||
|
console.log('here')
|
||||||
resolve()
|
resolve()
|
||||||
}
|
}
|
||||||
req.onerror = (event) => {
|
req.onerror = (event) => {
|
||||||
|
|
|
||||||
|
|
@ -422,6 +422,11 @@ export class Node {
|
||||||
getParent() {//{{{
|
getParent() {//{{{
|
||||||
return this._parent
|
return this._parent
|
||||||
}//}}}
|
}//}}}
|
||||||
|
moveToParent(newParentUUID) {// {{{
|
||||||
|
this.ParentUUID = newParentUUID
|
||||||
|
this.data.ParentUUID = newParentUUID
|
||||||
|
this._modified = true
|
||||||
|
}// }}}
|
||||||
isLastSibling() {//{{{
|
isLastSibling() {//{{{
|
||||||
return this._sibling_after === null
|
return this._sibling_after === null
|
||||||
}//}}}
|
}//}}}
|
||||||
|
|
@ -463,9 +468,10 @@ export class Node {
|
||||||
|
|
||||||
// When stored into database and ancestry was changed,
|
// When stored into database and ancestry was changed,
|
||||||
// the ancestry path could be interesting.
|
// the ancestry path could be interesting.
|
||||||
|
/*
|
||||||
const ancestors = await nodeStore.getNodeAncestry(this)
|
const ancestors = await nodeStore.getNodeAncestry(this)
|
||||||
this.data.Ancestors = ancestors.map(a => a.get('Name')).reverse()
|
this.data.Ancestors = ancestors.map(a => a.get('Name')).reverse()
|
||||||
|
*/
|
||||||
/* The node history is a local store for node history.
|
/* The node history is a local store for node history.
|
||||||
* This could be provisioned from the server or cleared if
|
* This could be provisioned from the server or cleared if
|
||||||
* deemed unnecessary.
|
* deemed unnecessary.
|
||||||
|
|
@ -481,12 +487,17 @@ export class Node {
|
||||||
const history = nodeStore.nodesHistory.add(this)
|
const history = nodeStore.nodesHistory.add(this)
|
||||||
|
|
||||||
// Updated node is added to the send queue to be stored on server.
|
// Updated node is added to the send queue to be stored on server.
|
||||||
|
|
||||||
const sendQueue = nodeStore.sendQueue.add(this)
|
const sendQueue = nodeStore.sendQueue.add(this)
|
||||||
|
|
||||||
// Updated node is saved to the primary node store.
|
// Updated node is saved to the primary node store.
|
||||||
const nodeStoreAdding = nodeStore.add([this])
|
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' })
|
treenode?.scrollIntoView({ block: 'nearest' })
|
||||||
}// }}}
|
}// }}}
|
||||||
}
|
}
|
||||||
customElements.define('n2-sidebar', N2Sidebar)
|
|
||||||
|
|
||||||
export class N2TreeNode extends CustomHTMLElement {
|
export class N2TreeNode extends CustomHTMLElement {
|
||||||
|
static DRAG_ICON = new Image()
|
||||||
|
static DRAG_ICON_OK = new Image()
|
||||||
|
|
||||||
static {// {{{
|
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 = document.createElement('template')
|
||||||
this.tmpl.innerHTML = `
|
this.tmpl.innerHTML = `
|
||||||
<style>
|
<style>
|
||||||
n2-sidebar:focus-within {
|
n2-sidebar:focus-within {
|
||||||
.el-name {
|
& > .el-name {
|
||||||
&.selected {
|
&.selected {
|
||||||
span {
|
span {
|
||||||
position:relative;
|
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>
|
</style>
|
||||||
|
|
||||||
<div data-el="expand-toggle" class="expand-toggle">
|
<div data-el="expand-toggle" class="expand-toggle">
|
||||||
<img data-el="expand">
|
<img data-el="expand" draggable="false">
|
||||||
</div>
|
</div>
|
||||||
<div data-el="name" class="name"><span></span></div>
|
<div data-el="name" class="name"><span></span></div>
|
||||||
<div data-el="children" class="children"></div>
|
<div data-el="children" class="children"></div>
|
||||||
|
|
@ -490,6 +545,7 @@ export class N2TreeNode extends CustomHTMLElement {
|
||||||
|
|
||||||
constructor(sidebar, node, parent) {//{{{
|
constructor(sidebar, node, parent) {//{{{
|
||||||
super()
|
super()
|
||||||
|
this.setAttribute('draggable', 'true')
|
||||||
this.classList.add('node')
|
this.classList.add('node')
|
||||||
|
|
||||||
this.sidebar = sidebar
|
this.sidebar = sidebar
|
||||||
|
|
@ -498,6 +554,7 @@ export class N2TreeNode extends CustomHTMLElement {
|
||||||
|
|
||||||
this.children_populated = false
|
this.children_populated = false
|
||||||
this.rendered = false
|
this.rendered = false
|
||||||
|
this.dragNode = null
|
||||||
|
|
||||||
this.elExpandToggle.addEventListener('click', () => this.sidebar.setNodeExpanded(this.node, !this.sidebar.getNodeExpanded(this.node.UUID)))
|
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))
|
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 => {
|
_mbus.subscribe(`NODE_EXPAND_${node.UUID}`, _state => {
|
||||||
this.render(true)
|
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) {//{{{
|
async fetchChildren(force_fetch) {//{{{
|
||||||
if (this.children_populated && !force_fetch)
|
if (this.children_populated && !force_fetch)
|
||||||
|
|
@ -575,6 +696,8 @@ export class N2TreeNode extends CustomHTMLElement {
|
||||||
img.setAttribute('src', newSrc)
|
img.setAttribute('src', newSrc)
|
||||||
}// }}}
|
}// }}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customElements.define('n2-sidebar', N2Sidebar)
|
||||||
customElements.define('n2-treenode', N2TreeNode)
|
customElements.define('n2-treenode', N2TreeNode)
|
||||||
|
|
||||||
// vim: foldmethod=marker
|
// vim: foldmethod=marker
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
{{ define "page" }}
|
{{ define "page" }}
|
||||||
|
|
||||||
|
<!-- Drag and drop elements -->
|
||||||
|
|
||||||
<!-- page-node -->
|
<!-- page-node -->
|
||||||
<div id="notes2" class="page-history">
|
<div id="notes2" class="page-history">
|
||||||
<div id="tree-expander" onclick="window._mbus.dispatch('TREE_EXPANSION', { expand: true })">></div>
|
<div id="tree-expander" onclick="window._mbus.dispatch('TREE_EXPANSION', { expand: true })">></div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue