diff --git a/datagraph b/datagraph index 0320ee8..30cb21a 100755 Binary files a/datagraph and b/datagraph differ diff --git a/node.go b/node.go index 065b4b8..2e064e5 100644 --- a/node.go +++ b/node.go @@ -165,3 +165,24 @@ func UpdateNode(nodeID int, data []byte) (err error) { _, err = db.Exec(`UPDATE public.node SET data=$2 WHERE id=$1`, nodeID, data) return } + +func CreateNode(parentNodeID, typeID int, name string) (err error) { + j, _ := json.Marshal(struct { Name string }{name}) + + row := db.QueryRow(` + INSERT INTO node(type_id, name, data) + VALUES($1, $2, $3::jsonb) + RETURNING id + `, + typeID, name, j) + + var id int + err = row.Scan(&id) + if err != nil { + return + } + + _, err = db.Exec(`INSERT INTO connection("parent", "child") VALUES($1, $2)`, parentNodeID, id) + + return +} diff --git a/static/css/main.css b/static/css/main.css index 0458aba..f00c2fc 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -21,14 +21,16 @@ body { [onClick] { cursor: pointer; } +.page { + display: none; +} +.page.show { + display: block; +} .section { background-color: #fff; padding: 32px; border-radius: 8px; - display: none; -} -.section.show { - display: block; } #layout { display: grid; @@ -43,7 +45,7 @@ body { grid-gap: 16px; align-items: center; } -#menu.section { +#menu.page { display: grid; padding: 16px 32px; } @@ -63,6 +65,8 @@ body { } #editor-node { grid-area: details; + display: grid; + grid-gap: 16px; } #types { grid-area: navigation; @@ -103,6 +107,12 @@ body { .node img { height: 24px; } +.node.selected > .name { + color: #a02c2c; +} +.node.selected > .type-icon { + filter: invert(0.7) sepia(0.5) hue-rotate(0deg) saturate(750%) brightness(0.85) !important; +} .node.expanded > .children { display: block; } @@ -123,7 +133,7 @@ body { padding-right: 4px; } .node .type-icon img { - filter: invert(0.7) sepia(0.5) hue-rotate(50deg) saturate(300%) brightness(0.85) !important; + filter: invert(0.7) sepia(0.5) hue-rotate(50deg) saturate(300%) brightness(0.85); } .node .name { margin-bottom: 8px; @@ -136,9 +146,33 @@ body { border-left: 1px solid #ccc; padding-left: 12px; margin-left: 19px; - /* - &.expanded { - display: block; - } - */ +} +select { + font-size: 1em; + border: 1px solid #bcc3ce; + background: #fff; + color: #444; + padding: 4px; +} +select optgroup { + color: #a22; +} +datalist div:before { + display: block; + content: 'group'; + font-weight: bold; +} +dialog#create-type { + min-width: 400px; +} +dialog#create-type > div { + display: grid; + grid-gap: 8px; +} +dialog::backdrop { + background-color: rgba(0, 0, 0, 0.75); +} +select:focus { + outline: 2px solid #888; + outline-offset: -2px; } diff --git a/static/js/app.mjs b/static/js/app.mjs index 1b9080c..930ad14 100644 --- a/static/js/app.mjs +++ b/static/js/app.mjs @@ -7,27 +7,39 @@ export class App { this.editor = null this.typesList = null this.currentNodeID = null + this.types = [] + this.currentPage = null const events = [ 'MENU_ITEM_SELECTED', 'NODE_SELECTED', 'EDITOR_NODE_SAVE', 'TYPES_LIST_FETCHED', + 'NODE_CREATE_DIALOG', + 'NODE_CREATE', ] for (const eventName of events) mbus.subscribe(eventName, event => this.eventHandler(event)) + document.addEventListener('keydown', event => this.keyHandler(event)) + mbus.dispatch('MENU_ITEM_SELECTED', 'node') }// }}} - eventHandler(event) {// {{{ + async eventHandler(event) {// {{{ switch (event.type) { case 'MENU_ITEM_SELECTED': const item = document.querySelector(`#menu [data-section="${event.detail}"]`) - this.section(item, event.detail) + this.page(item, event.detail) break case 'NODE_SELECTED': + for (const n of document.querySelectorAll('#nodes .node.selected')) + n.classList.remove('selected') + + for (const n of document.querySelectorAll(`#nodes .node[data-node-id="${event.detail}"]`)) + n.classList?.add('selected') + this.currentNodeID = event.detail this.edit(this.currentNodeID) break @@ -40,20 +52,49 @@ export class App { const types = document.getElementById('types') types.replaceChildren(this.typesList.render()) + case 'NODE_CREATE_DIALOG': + if (this.currentPage !== 'node' || this.currentNodeID === null) + return + + new NodeCreateDialog(this.currentNodeID) + break + + case 'NODE_CREATE': break default: console.log(event) } }// }}} - section(item, name) {// {{{ + keyHandler(event) {// {{{ + let handled = true + + switch (event.key.toUpperCase()) { + case 'N': + if (!event.shiftKey || !event.altKey) + break + mbus.dispatch('NODE_CREATE_DIALOG') + break + + default: + handled = false + } + + if (handled) { + event.stopPropagation() + event.preventDefault() + } + }// }}} + page(item, name) {// {{{ for (const el of document.querySelectorAll('#menu .item')) el.classList.remove('selected') item.classList.add('selected') - for (const el of document.querySelectorAll('.section.show')) + for (const el of document.querySelectorAll('.page.show')) el.classList.remove('show') + this.currentPage = name + switch (name) { case 'node': document.getElementById('nodes').classList.add('show') @@ -112,13 +153,126 @@ export class App { const timePassed = Date.now() - buttonPressed if (timePassed < 250) - setTimeout(()=>btn.disabled = false, 250 - timePassed) + setTimeout(() => btn.disabled = false, 250 - timePassed) else btn.disabled = false }) }// }}} } +class NodeCreateDialog { + constructor(parentNodeID) {// {{{ + this.parentNodeID = parentNodeID + this.dialog = null + this.types = null + this.select = null + this.input = null + + this.createElements() + + this.fetchTypes() + .then(() => { + const st = new SelectType(this.types) + this.select.replaceChildren(st.render()) + }) + + this.dialog.showModal() + this.select.focus() + }// }}} + createElements() {// {{{ + this.dialog = document.createElement('dialog') + this.dialog.id = 'create-type' + this.dialog.innerHTML = ` +