From d0f410323eef9e10f220d74bbbedfee0e9ece772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Fri, 12 Jan 2024 21:31:32 +0100 Subject: [PATCH] Checklist management without reordering --- main.go | 146 ++++++++++++++- node.go | 158 +++++++++++++++- static/css/main.css | 82 +++++++- static/images/add-gray.svg | 76 ++++++++ static/images/edit-list-gray.svg | 66 +++++++ static/images/edit-list.svg | 66 +++++++ static/images/edit.svg | 66 +++++++ static/images/trashcan.svg | 66 +++++++ static/js/app.mjs | 4 +- static/js/checklist.mjs | 310 ++++++++++++++++++++++++++++--- static/js/node.mjs | 4 + static/less/main.less | 102 +++++++++- 12 files changed, 1100 insertions(+), 46 deletions(-) create mode 100644 static/images/add-gray.svg create mode 100644 static/images/edit-list-gray.svg create mode 100644 static/images/edit-list.svg create mode 100644 static/images/edit.svg create mode 100644 static/images/trashcan.svg diff --git a/main.go b/main.go index 64443a4..6981700 100644 --- a/main.go +++ b/main.go @@ -104,7 +104,13 @@ func main() { // {{{ service.Register("/node/delete", true, true, nodeDelete) service.Register("/node/download", true, true, nodeDownload) service.Register("/node/search", true, true, nodeSearch) + service.Register("/node/checklist_group/add", true, true, nodeChecklistGroupAdd) + service.Register("/node/checklist_group/item_add", true, true, nodeChecklistGroupItemAdd) + service.Register("/node/checklist_group/label", true, true, nodeChecklistGroupLabel) + service.Register("/node/checklist_group/delete", true, true, nodeChecklistGroupDelete) service.Register("/node/checklist_item/state", true, true, nodeChecklistItemState) + service.Register("/node/checklist_item/label", true, true, nodeChecklistItemLabel) + service.Register("/node/checklist_item/delete", true, true, nodeChecklistItemDelete) service.Register("/key/retrieve", true, true, keyRetrieve) service.Register("/key/create", true, true, keyCreate) service.Register("/key/counter", true, true, keyCounter) @@ -478,8 +484,102 @@ func nodeSearch(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{ "Nodes": nodes, }) } // }}} +func nodeChecklistGroupAdd(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{ + var err error + + req := struct { + NodeID int + Label string + }{} + if err = parseRequest(r, &req); err != nil { + responseError(w, err) + return + } + + var group ChecklistGroup + group, err = ChecklistGroupAdd(sess.UserID, req.NodeID, req.Label) + if err != nil { + responseError(w, err) + return + } + group.Items = []ChecklistItem{} + + responseData(w, map[string]interface{}{ + "OK": true, + "Group": group, + }) +} // }}} +func nodeChecklistGroupItemAdd(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{ + var err error + + req := struct { + ChecklistGroupID int + Label string + }{} + if err = parseRequest(r, &req); err != nil { + responseError(w, err) + return + } + + var item ChecklistItem + item, err = ChecklistGroupItemAdd(sess.UserID, req.ChecklistGroupID, req.Label) + if err != nil { + responseError(w, err) + return + } + + responseData(w, map[string]interface{}{ + "OK": true, + "Item": item, + }) +} // }}} +func nodeChecklistGroupLabel(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{ + var err error + + req := struct { + ChecklistGroupID int + Label string + }{} + if err = parseRequest(r, &req); err != nil { + responseError(w, err) + return + } + + var item ChecklistItem + item, err = ChecklistGroupLabel(sess.UserID, req.ChecklistGroupID, req.Label) + if err != nil { + responseError(w, err) + return + } + + responseData(w, map[string]interface{}{ + "OK": true, + "Item": item, + }) +} // }}} +func nodeChecklistGroupDelete(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{ + var err error + + req := struct { + ChecklistGroupID int + }{} + if err = parseRequest(r, &req); err != nil { + responseError(w, err) + return + } + + err = ChecklistGroupDelete(sess.UserID, req.ChecklistGroupID) + if err != nil { + logger.Error("checklist", "error", err) + responseError(w, err) + return + } + + responseData(w, map[string]interface{}{ + "OK": true, + }) +} // }}} func nodeChecklistItemState(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{ - logger.Info("webserver", "request", "/node/checklist_item/state") var err error req := struct { @@ -501,6 +601,50 @@ func nodeChecklistItemState(w http.ResponseWriter, r *http.Request, sess *sessio "OK": true, }) } // }}} +func nodeChecklistItemLabel(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{ + var err error + + req := struct { + ChecklistItemID int + Label string + }{} + if err = parseRequest(r, &req); err != nil { + responseError(w, err) + return + } + + err = ChecklistItemLabel(sess.UserID, req.ChecklistItemID, req.Label) + if err != nil { + responseError(w, err) + return + } + + responseData(w, map[string]interface{}{ + "OK": true, + }) +} // }}} +func nodeChecklistItemDelete(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{ + var err error + + req := struct { + ChecklistItemID int + }{} + if err = parseRequest(r, &req); err != nil { + responseError(w, err) + return + } + + err = ChecklistItemDelete(sess.UserID, req.ChecklistItemID) + if err != nil { + logger.Error("checklist", "error", err) + responseError(w, err) + return + } + + responseData(w, map[string]interface{}{ + "OK": true, + }) +} // }}} func keyRetrieve(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{ logger.Info("webserver", "request", "/key/retrieve") diff --git a/node.go b/node.go index 4cc55e8..f11c351 100644 --- a/node.go +++ b/node.go @@ -441,7 +441,111 @@ func SearchNodes(userID int, search string) (nodes []Node, err error) { // {{{ return } // }}} -func ChecklistItemState(userID, checklistItemID int, state bool) (err error) {// {{{ + +func ChecklistGroupAdd(userID, nodeID int, label string) (item ChecklistGroup, err error) { // {{{ + var row *sqlx.Row + row = service.Db.Conn.QueryRowx( + ` + INSERT INTO checklist_group(node_id, "order", "label") + ( + SELECT + $1, + MAX("order")+1 AS "order", + $2 AS "label" + FROM checklist_group g + INNER JOIN node n ON g.node_id = n.id + WHERE + user_id = $3 AND + node_id = $1 + GROUP BY + node_id + ) UNION ( + SELECT + node.id AS node_id, + 0 AS "order", + $2 AS "label" + FROM node + WHERE + user_id = $3 AND + node.id = $1 + ) + ORDER BY "order" DESC + LIMIT 1 + RETURNING + * + `, + nodeID, + label, + userID, + ) + err = row.StructScan(&item) + return +} // }}} +func ChecklistGroupLabel(userID, checklistGroupID int, label string) (item ChecklistItem, err error) { // {{{ + _, err = service.Db.Conn.Exec( + ` + UPDATE checklist_group g + SET label = $3 + FROM node n + WHERE + g.node_id = n.id AND + n.user_id = $1 AND + g.id = $2; + `, + userID, + checklistGroupID, + label, + ) + return +} // }}} +func ChecklistGroupItemAdd(userID, checklistGroupID int, label string) (item ChecklistItem, err error) { // {{{ + var row *sqlx.Row + row = service.Db.Conn.QueryRowx( + ` + INSERT INTO checklist_item(checklist_group_id, "order", "label") + ( + SELECT + checklist_group_id, + MAX("order")+1 AS "order", + $1 AS "label" + FROM checklist_item + WHERE + checklist_group_id = $2 + GROUP BY + checklist_group_id + ) UNION ( + SELECT $2 AS checklist_group_id, 0 AS "order", $1 AS "label" + ) + ORDER BY "order" DESC + LIMIT 1 + RETURNING + * + `, + label, + checklistGroupID, + ) + err = row.StructScan(&item) + return +} // }}} +func ChecklistGroupDelete(userID, checklistGroupID int) (err error) { // {{{ + _, err = service.Db.Conn.Exec( + ` + DELETE + FROM checklist_group g + USING + node n + WHERE + g.id = $2 AND + g.node_id = n.id AND + n.user_id = $1 + `, + userID, + checklistGroupID, + ) + return +} // }}} + +func ChecklistItemState(userID, checklistItemID int, state bool) (err error) { // {{{ _, err = service.Db.Conn.Exec( ` UPDATE checklist_item i @@ -458,7 +562,44 @@ func ChecklistItemState(userID, checklistItemID int, state bool) (err error) {// state, ) return -}// }}} +} // }}} +func ChecklistItemLabel(userID, checklistItemID int, label string) (err error) { // {{{ + _, err = service.Db.Conn.Exec( + ` + UPDATE checklist_item i + SET label = $3 + FROM checklist_group g, node n + WHERE + i.checklist_group_id = g.id AND + g.node_id = n.id AND + n.user_id = $1 AND + i.id = $2; + `, + userID, + checklistItemID, + label, + ) + return +} // }}} +func ChecklistItemDelete(userID, checklistItemID int) (err error) { // {{{ + _, err = service.Db.Conn.Exec( + ` + DELETE + FROM checklist_item i + USING + checklist_group g, + node n + WHERE + i.id = $2 AND + i.checklist_group_id = g.id AND + g.node_id = n.id AND + n.user_id = $1 + `, + userID, + checklistItemID, + ) + return +} // }}} func (node *Node) retrieveChecklist() (err error) { // {{{ var rows *sqlx.Rows @@ -468,10 +609,10 @@ func (node *Node) retrieveChecklist() (err error) { // {{{ g.order AS group_order, g.label AS group_label, - i.id AS item_id, - i.order AS item_order, - i.label AS item_label, - i.checked + COALESCE(i.id, 0) AS item_id, + COALESCE(i.order, 0) AS item_order, + COALESCE(i.label, '') AS item_label, + COALESCE(i.checked, false) AS checked FROM public.checklist_group g LEFT JOIN public.checklist_item i ON i.checklist_group_id = g.id @@ -522,7 +663,10 @@ func (node *Node) retrieveChecklist() (err error) { // {{{ item.Order = row.ItemOrder item.Label = row.ItemLabel item.Checked = row.Checked - group.Items = append(group.Items, item) + + if item.ID > 0 { + group.Items = append(group.Items, item) + } } node.ChecklistGroups = []ChecklistGroup{} diff --git a/static/css/main.css b/static/css/main.css index 3fbf444..398d55a 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -330,6 +330,11 @@ header .menu { padding: 4px; border-radius: 4px; } +#markdown pre { + background: #f5f5f5; + padding: 8px; + border-radius: 8px; +} #markdown pre > code { background: unset; padding: 0px; @@ -346,27 +351,72 @@ header .menu { margin-top: 8px; margin-bottom: 0px; } +#checklist .header { + display: grid; + grid-template-columns: repeat(3, min-content); + align-items: center; + grid-gap: 0 16px; +} +#checklist .header img { + height: 20px; + cursor: pointer; +} +#checklist .header + .checklist-group.edit { + margin-top: 16px; +} #checklist .checklist-group { + display: grid; + grid-template-columns: repeat(4, min-content); + align-items: center; + grid-gap: 0 8px; margin-top: 1em; + margin-bottom: 8px; font-weight: bold; } +#checklist .checklist-group .label { + white-space: nowrap; +} +#checklist .checklist-group .label.ok { + color: #54b356; +} +#checklist .checklist-group .label.error { + color: #d13636; +} +#checklist .checklist-group.edit { + margin-top: 32px; + border-bottom: 1px solid #aaa; + padding-bottom: 8px; +} +#checklist .checklist-group:not(.edit) .reorder { + display: none; +} +#checklist .checklist-group:not(.edit) img { + display: none; +} +#checklist .checklist-group img { + height: 14px; + cursor: pointer; +} #checklist .checklist-item { display: grid; - grid-template-columns: min-content 1fr; + grid-template-columns: repeat(3, min-content); + grid-gap: 0 8px; align-items: center; - margin-top: 0.5em; + padding: 4px 0; + border-bottom: 2px solid #fff; } #checklist .checklist-item.checked { text-decoration: line-through; - color: #888; + color: #aaa; +} +#checklist .checklist-item.drag-target { + border-bottom: 2px solid #71c837; } #checklist .checklist-item input[type="checkbox"] { - margin-left: 0px; - margin-right: 8px; -webkit-appearance: none; appearance: none; background-color: #fff; - margin: 0 8px 0 0; + margin: 0 2px 0 0; font: inherit; color: currentColor; width: 1.25em; @@ -377,6 +427,12 @@ header .menu { display: grid; place-content: center; } +#checklist .checklist-item label.ok { + color: #54b356; +} +#checklist .checklist-item label.error { + color: #d13636; +} #checklist .checklist-item input[type="checkbox"].ok { border: 0.15em solid #54b356; } @@ -400,8 +456,22 @@ header .menu { #checklist .checklist-item input[type="checkbox"]:checked::before { transform: scale(1); } +#checklist .checklist-item.edit input[type="checkbox"] { + margin-left: 8px; +} +#checklist .checklist-item:not(.edit) .reorder { + display: none; +} +#checklist .checklist-item:not(.edit) img { + display: none; +} +#checklist .checklist-item img { + height: 14px; + cursor: pointer; +} #checklist .checklist-item label { user-select: none; + white-space: nowrap; } /* ============================================================= * * Textarea replicates the height of an element expanding height * diff --git a/static/images/add-gray.svg b/static/images/add-gray.svg new file mode 100644 index 0000000..b876269 --- /dev/null +++ b/static/images/add-gray.svg @@ -0,0 +1,76 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/static/images/edit-list-gray.svg b/static/images/edit-list-gray.svg new file mode 100644 index 0000000..8289ef3 --- /dev/null +++ b/static/images/edit-list-gray.svg @@ -0,0 +1,66 @@ + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/static/images/edit-list.svg b/static/images/edit-list.svg new file mode 100644 index 0000000..3b28876 --- /dev/null +++ b/static/images/edit-list.svg @@ -0,0 +1,66 @@ + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/static/images/edit.svg b/static/images/edit.svg new file mode 100644 index 0000000..81014fe --- /dev/null +++ b/static/images/edit.svg @@ -0,0 +1,66 @@ + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/static/images/trashcan.svg b/static/images/trashcan.svg new file mode 100644 index 0000000..120cda9 --- /dev/null +++ b/static/images/trashcan.svg @@ -0,0 +1,66 @@ + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/static/js/app.mjs b/static/js/app.mjs index d22d754..8a486bf 100644 --- a/static/js/app.mjs +++ b/static/js/app.mjs @@ -1,10 +1,12 @@ +import 'preact/debug' import 'preact/devtools' -import { signal } from 'preact/signals' + import { h, Component, render, createRef } from 'preact' import htm from 'htm' import { Session } from 'session' import { Node, NodeUI } from 'node' import { Websocket } from 'ws' +import { signal } from 'preact/signals' const html = htm.bind(h) class App extends Component { diff --git a/static/js/checklist.mjs b/static/js/checklist.mjs index c3c5844..6abfbdc 100644 --- a/static/js/checklist.mjs +++ b/static/js/checklist.mjs @@ -12,13 +12,47 @@ export class ChecklistGroup { constructor(data) {//{{{ Object.keys(data).forEach(key => { if (key == 'Items') - this.items = data[key].map(itemData => - new ChecklistItem(itemData) + this.items = data[key].map(itemData => { + let item = new ChecklistItem(itemData) + item.checklistGroup = this + return item + } ).sort(ChecklistItem.sort) else this[key] = data[key] }) }//}}} + addItem(label, okCallback) {//{{{ + window._app.current.request('/node/checklist_group/item_add', { + ChecklistGroupID: this.ID, + Label: label, + }) + .then(json => { + let item = new ChecklistItem(json.Item) + item.checklistGroup = this + this.items.push(item) + okCallback() + }) + .catch(window._app.current.responseError) + return + }//}}} + updateLabel(newLabel, okCallback, errCallback) {//{{{ + window._app.current.request('/node/checklist_group/label', { + ChecklistGroupID: this.ID, + Label: newLabel, + }) + .then(okCallback) + .catch(errCallback) + }//}}} + delete(okCallback, errCallback) {//{{{ + window._app.current.request('/node/checklist_group/delete', { + ChecklistGroupID: this.ID, + }) + .then(() => { + okCallback() + }) + .catch(errCallback) + }//}}} } export class ChecklistItem { @@ -32,34 +66,193 @@ export class ChecklistItem { this[key] = data[key] }) }//}}} + updateState(newState, okCallback, errCallback) {//{{{ + window._app.current.request('/node/checklist_item/state', { + ChecklistItemID: this.ID, + State: newState, + }) + .then(okCallback) + .catch(errCallback) + }//}}} + updateLabel(newLabel, okCallback, errCallback) {//{{{ + window._app.current.request('/node/checklist_item/label', { + ChecklistItemID: this.ID, + Label: newLabel, + }) + .then(okCallback) + .catch(errCallback) + }//}}} + delete(okCallback, errCallback) {//{{{ + window._app.current.request('/node/checklist_item/delete', { + ChecklistItemID: this.ID, + }) + .then(() => { + this.checklistGroup.items = this.checklistGroup.items.filter(item => item.ID != this.ID) + okCallback() + }) + .catch(errCallback) + }//}}} } export class Checklist extends Component { - render({ groups }) {//{{{ + constructor() {//{{{ + super() + this.edit = signal(true) + this.dragItemSource = null + this.dragItemTarget = null + this.state = { + confirmDeletion: true, + } + }//}}} + render({ groups }, { confirmDeletion }) {//{{{ if (groups.length == 0) return - groups.sort(ChecklistGroup.sort) + if (typeof groups.sort != 'function') + groups = [] - let groupElements = groups.map(group => html`<${ChecklistGroupElement} group=${group} />`) + groups.sort(ChecklistGroup.sort) + let groupElements = groups.map(group => html`<${ChecklistGroupElement} key="group-${group.ID}" ui=${this} group=${group} />`) + + let edit = 'edit-list-gray.svg' + let confirmDeletionEl = '' + if (this.edit.value) { + edit = 'edit-list.svg' + confirmDeletionEl = html` +
+ this.setState({ confirmDeletion: !confirmDeletion })} /> + +
+ ` + } + + let addGroup = ()=>{ + if (this.edit.value) + return html`this.addGroup()} />` + } return html`
-

Checklist

+
+

Checklist

+ this.toggleEdit()} /> + <${addGroup} /> +
+ ${confirmDeletionEl} ${groupElements}
` }//}}} + + toggleEdit() {//{{{ + this.edit.value = !this.edit.value + }//}}} + addGroup() {//{{{ + let label = prompt("Create a new group") + if (label === null) + return + label = label.trim() + if (label == '') + return + + window._app.current.request('/node/checklist_group/add', { + NodeID: window._app.current.nodeUI.current.node.value.ID, + Label: label, + }) + .then(json => { + let group = new ChecklistGroup(json.Group) + this.props.groups.push(group) + this.forceUpdate() + }) + .catch(window._app.current.responseError) + return + }//}}} + dragTarget(target) {//{{{ + if (this.dragItemTarget) + this.dragItemTarget.setDragTarget(false) + this.dragItemTarget = target + target.setDragTarget(true) + }//}}} + dragReset() {//{{{ + if (this.dragItemTarget) { + this.dragItemTarget.setDragTarget(false) + this.dragItemTarget = null + } + }//}}} } class ChecklistGroupElement extends Component { - render({ group }) {//{{{ - let items = group.items.map(item => html`<${ChecklistItemElement} item=${item} />`) + constructor() {//{{{ + super() + this.label = createRef() + }//}}} + render({ ui, group }) {//{{{ + let items = ({ ui, group }) => + group.items.map(item => html`<${ChecklistItemElement} key="item-${item.ID}" ui=${ui} group=${this} item=${item} />`) + + let label = ()=>html`
this.editLabel()}>${group.Label}
` + return html` -
${group.Label}
- ${items} +
+
+
+ this.delete()} /> + <${label} /> + this.addItem()} /> +
+ <${items} ui=${ui} group=${group} /> +
` }//}}} + addItem() {//{{{ + let label = prompt("Create a new item") + if (label === null) + return + label = label.trim() + if (label == '') + return + + this.props.group.addItem(label, () => { + this.forceUpdate() + }) + }//}}} + editLabel() {//{{{ + let label = prompt('Edit label', this.props.group.Label) + if (label === null) + return + + label = label.trim() + if (label == '') { + alert(`A label can't be empty.`) + return + } + + this.label.current.classList.remove('error') + this.props.group.updateLabel(label, () => { + this.props.group.Label = label + this.label.current.innerHTML = label + this.label.current.classList.add('ok') + this.forceUpdate() + setTimeout(() => this.label.current.classList.remove('ok'), 500) + }, () => { + this.label.current.classList.add('error') + }) + + }//}}} + delete() {//{{{ + if (this.props.ui.state.confirmDeletion) { + if (!confirm(`Delete '${this.props.group.Label}'?`)) + return + } + + this.props.group.delete(() => { + this.props.ui.props.groups = this.props.ui.props.groups.filter(g => g.ID != this.props.group.ID) + this.props.ui.forceUpdate() + }, err => { + console.log(err) + console.log('error') + }) + }//}}} } class ChecklistItemElement extends Component { @@ -67,31 +260,100 @@ class ChecklistItemElement extends Component { super(props) this.state = { checked: props.item.Checked, + dragTarget: false, } this.checkbox = createRef() + this.label = createRef() }//}}} - render({ item }, { checked }) {//{{{ + render({ ui, item }, { checked, dragTarget }) {//{{{ + let checkbox = ()=>{ + if (ui.edit.value) + return html`` + else + return html` + this.update(evt.target.checked)} /> + + ` + } return html` -
- this.update(evt.target.checked)} /> - +
+
+ this.delete()} /> + <${checkbox} />
` }//}}} + componentDidMount() {//{{{ + this.base.addEventListener('dragstart', ()=>this.dragStart()) + this.base.addEventListener('dragend', ()=>this.dragEnd()) + this.base.addEventListener('dragenter', evt=>this.dragEnter(evt)) + }//}}} + update(checked) {//{{{ this.setState({ checked }) this.checkbox.current.classList.remove('error') - window._app.current.request('/node/checklist_item/state', { - ChecklistItemID: this.props.item.ID, - State: checked, + this.props.item.updateState(checked, () => { + this.checkbox.current.classList.add('ok') + setTimeout(() => this.checkbox.current.classList.remove('ok'), 500) + }, () => { + this.checkbox.current.classList.add('error') }) - .then(res => { - this.checkbox.current.classList.add('ok') - setTimeout(()=>this.checkbox.current.classList.remove('ok'), 500) + }//}}} + editLabel() {//{{{ + let label = prompt('Edit label', this.props.item.Label) + if (label === null) + return - }) - .catch(()=>{ - this.checkbox.current.classList.add('error') - }) + label = label.trim() + if (label == '') { + alert(`A label can't be empty.`) + return + } + + this.label.current.classList.remove('error') + this.props.item.updateLabel(label, () => { + this.props.item.Label = label + this.label.current.innerHTML = label + this.label.current.classList.add('ok') + setTimeout(() => this.label.current.classList.remove('ok'), 500) + }, () => { + this.label.current.classList.add('error') + }) + + }//}}} + delete() {//{{{ + if (this.props.ui.state.confirmDeletion) { + if (!confirm(`Delete '${this.props.item.Label}'?`)) + return + } + + this.props.item.delete(() => { + this.props.group.forceUpdate() + }, err => { + console.log(err) + console.log('error') + }) + }//}}} + + setDragTarget(state) {//{{{ + this.setState({ dragTarget: state }) + }//}}} + dragStart() {//{{{ + // Shouldn't be needed, but in case the previous drag was bungled up, we reset. + this.props.ui.dragReset() + this.props.ui.dragItemSource = this + }//}}} + dragEnter(evt) {//{{{ + evt.preventDefault() + this.props.ui.dragTarget(this) + }//}}} + dragEnd() {//{{{ + console.log( + this.props.ui.dragItemSource.props.item.Label, + this.props.ui.dragItemTarget.props.item.Label, + ) + this.props.ui.dragReset() }//}}} } + +// vim: foldmethod=marker diff --git a/static/js/node.mjs b/static/js/node.mjs index 32d879c..c1d6f94 100644 --- a/static/js/node.mjs +++ b/static/js/node.mjs @@ -146,6 +146,10 @@ export class NodeUI extends Component { return switch (evt.key.toUpperCase()) { + case 'C': + this.showPage('node') + break + case 'E': this.showPage('keys') break diff --git a/static/less/main.less b/static/less/main.less index 0c95c31..ca10433 100644 --- a/static/less/main.less +++ b/static/less/main.less @@ -379,6 +379,12 @@ header { border-radius: 4px; } + pre { + background: #f5f5f5; + padding: 8px; + border-radius: 8px; + } + pre > code { background: unset; padding: 0px; @@ -398,31 +404,83 @@ header { margin-top: 8px; margin-bottom: 0px; + .header { + display: grid; + grid-template-columns: repeat(3, min-content); + align-items: center; + grid-gap: 0 16px; + + img { + height: 20px; + cursor: pointer; + } + } + + .header + .checklist-group.edit { + margin-top: 16px; + } + .checklist-group { + display: grid; + grid-template-columns: repeat(4, min-content); + align-items: center; + grid-gap: 0 8px; + margin-top: 1em; + margin-bottom: 8px; font-weight: bold; + .label { + white-space: nowrap; + } + + .label.ok { + color: #54b356; + } + + .label.error { + color: #d13636; + } + + &.edit { + margin-top: 32px; + border-bottom: 1px solid #aaa; + padding-bottom: 8px; + } + + &:not(.edit) { + .reorder { display: none; } + img { display: none; } + } + + img { + height: 14px; + cursor: pointer; + } } .checklist-item { display: grid; - grid-template-columns: min-content 1fr; + grid-template-columns: repeat(3, min-content); + grid-gap: 0 8px; align-items: center; - margin-top: 0.50em; + padding: 4px 0; + border-bottom: 2px solid #fff; &.checked { text-decoration: line-through; - color: #888; + color: #aaa; + } + + &.drag-target { + border-bottom: 2px solid #71c837; } input[type="checkbox"] { - margin-left: 0px; - margin-right: 8px; - -webkit-appearance: none; appearance: none; background-color: #fff; - margin: 0 8px 0 0; + margin: 0 2px 0 0; font: inherit; color: currentColor; width: 1.25em; @@ -435,6 +493,14 @@ header { place-content: center; } + label.ok { + color: #54b356; + } + + label.error { + color: #d13636; + } + input[type="checkbox"].ok { border: 0.15em solid #54b356; } @@ -464,8 +530,30 @@ header { transform: scale(1); } + &.edit { + input[type="checkbox"] { + margin-left: 8px; + } + } + + &:not(.edit) { + .reorder { + display: none; + } + img { + display: none; + } + } + + img { + height: 14px; + cursor: pointer; + } + + label { user-select: none; + white-space: nowrap; } } }