import { h, Component, createRef } from 'preact' import htm from 'htm' import { signal } from 'preact/signals' const html = htm.bind(h) export class ChecklistGroup { static sort(a, b) {//{{{ if (a.Order < b.Order) return -1 if (a.Order > b.Order) return 1 return 0 }//}}} constructor(data) {//{{{ Object.keys(data).forEach(key => { if (key == 'Items') this.items = data[key].map(itemData => { let item = new ChecklistItem(itemData) item.checklistGroup = this return item }) 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 { static sort(a, b) {//{{{ if (a.Order < b.Order) return -1 if (a.Order > b.Order) return 1 return 0 }//}}} constructor(data) {//{{{ Object.keys(data).forEach(key => { 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) }//}}} move(to, okCallback) {//{{{ window._app.current.request('/node/checklist_item/move', { ChecklistItemID: this.ID, AfterItemID: to.ID, }) .then(okCallback) .catch(_app.current.responseError) }//}}} } export class Checklist extends Component { constructor() {//{{{ super() this.edit = signal(false) this.dragItemSource = null this.dragItemTarget = null this.groupElements = {} this.state = { confirmDeletion: true, continueAddingItems: true, } window._checklist = this }//}}} render({ ui, groups }, { confirmDeletion, continueAddingItems }) {//{{{ this.groupElements = {} if (groups.length == 0 && !ui.node.value.ShowChecklist.value) return if (typeof groups.sort != 'function') groups = [] groups.sort(ChecklistGroup.sort) let groupElements = groups.map(group => { this.groupElements[group.ID] = createRef() return html`<${ChecklistGroupElement} ref=${this.groupElements[group.ID]} 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 })} />
this.setState({ continueAddingItems: !continueAddingItems })} />
` } let addGroup = () => { if (this.edit.value) return html` this.addGroup()} />` } return html`

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 InputElement extends Component { render({ placeholder, label }) {//{{{ return html`
${label}
` }//}}} componentDidMount() {//{{{ const dlg = document.getElementById('input-text') const input = document.getElementById('input-text-el') dlg.showModal() dlg.addEventListener("keydown", evt => this.keyhandler(evt)) input.addEventListener("keydown", evt => this.keyhandler(evt)) input.focus() }//}}} ok() {//{{{ const input = document.getElementById('input-text-el') this.props.callback(true, input.value) }//}}} cancel() {//{{{ this.props.callback(false) }//}}} keyhandler(evt) {//{{{ let handled = true switch (evt.key) { case 'Enter': this.ok() break; case 'Escape': this.cancel() break; default: handled = false } if (handled) { evt.stopPropagation() evt.preventDefault() } }//}}} } class ChecklistGroupElement extends Component { constructor() {//{{{ super() this.label = createRef() this.addingItem = signal(false) }//}}} render({ ui, group }) {//{{{ let items = ({ ui, group }) => group.items .sort(ChecklistItem.sort) .map(item => html`<${ChecklistItemElement} key="item-${item.ID}" ui=${ui} group=${this} item=${item} />`) let label = () => html`
this.editLabel()}>${group.Label}
` let addItem = () => { if (this.addingItem.value) return html`<${InputElement} label="New item" callback=${(ok, val) => this.addItem(ok, val)} />` } return html` <${addItem} />
this.delete()} /> <${label} /> this.addingItem.value = true} />
<${items} ui=${ui} group=${group} />
` }//}}} addItem(ok, label) {//{{{ if (!ok) { this.addingItem.value = false return } label = label.trim() if (label == '') { this.addingItem.value = false return } this.props.group.addItem(label, () => { this.forceUpdate() }) if (!this.props.ui.state.continueAddingItems) this.addingItem.value = false }//}}} 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 { constructor(props) {//{{{ super(props) this.state = { checked: props.item.Checked, dragTarget: false, } this.checkbox = createRef() this.label = createRef() }//}}} render({ ui, item }, { checked, dragTarget }) {//{{{ let checkbox = () => { if (ui.edit.value) return html`` else return html` this.update(evt.target.checked)} /> ` } return html`
this.delete()} /> <${checkbox} />
` }//}}} componentDidMount() {//{{{ this.base.addEventListener('dragstart', evt => this.dragStart(evt)) 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') 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') }) }//}}} editLabel() {//{{{ let label = prompt('Edit label', this.props.item.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.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(evt) {//{{{ // Shouldn't be needed, but in case the previous drag was bungled up, we reset. this.props.ui.dragReset() this.props.ui.dragItemSource = this const img = new Image(); evt.dataTransfer.setDragImage(img, 10, 10); }//}}} dragEnter(evt) {//{{{ evt.preventDefault() this.props.ui.dragTarget(this) }//}}} dragEnd() {//{{{ let groups = this.props.ui.props.groups let from = this.props.ui.dragItemSource.props.item let to = this.props.ui.dragItemTarget.props.item this.props.ui.dragReset() if (from.ID == to.ID) return let fromGroup = groups.find(g => g.ID == from.GroupID) let toGroup = groups.find(g => g.ID == to.GroupID) from.Order = to.Order from.GroupID = toGroup.ID toGroup.items.forEach(i => { if (i.ID == from.ID) return if (i.Order <= to.Order) i.Order-- }) if (fromGroup.ID != toGroup.ID) { fromGroup.items = fromGroup.items.filter(i => i.ID != from.ID) toGroup.items.push(from) } this.props.ui.groupElements[fromGroup.ID].current.forceUpdate() this.props.ui.groupElements[toGroup.ID].current.forceUpdate() from.move(to, () => {}) }//}}} } // vim: foldmethod=marker