wip
This commit is contained in:
parent
bd4a475923
commit
9a164b984a
36 changed files with 2500 additions and 77 deletions
472
static/js/checklist.mjs
Normal file
472
static/js/checklist.mjs
Normal file
|
|
@ -0,0 +1,472 @@
|
|||
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`
|
||||
<div>
|
||||
<input type="checkbox" id="confirm-checklist-delete" checked=${confirmDeletion} onchange=${() => this.setState({ confirmDeletion: !confirmDeletion })} />
|
||||
<label for="confirm-checklist-delete">Confirm checklist deletion</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="continue-adding-items" checked=${continueAddingItems} onchange=${() => this.setState({ continueAddingItems: !continueAddingItems })} />
|
||||
<label for="continue-adding-items">Continue adding items</label>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
let addGroup = () => {
|
||||
if (this.edit.value)
|
||||
return html`<img src="/images/${_VERSION}/add-gray.svg" onclick=${() => this.addGroup()} />`
|
||||
}
|
||||
|
||||
return html`
|
||||
<div id="checklist">
|
||||
<div class="header">
|
||||
<h1>Checklist</h1>
|
||||
<img src="/images/${_VERSION}/${edit}" onclick=${() => this.toggleEdit()} />
|
||||
<${addGroup} />
|
||||
</div>
|
||||
${confirmDeletionEl}
|
||||
${groupElements}
|
||||
</div>
|
||||
`
|
||||
}//}}}
|
||||
|
||||
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`
|
||||
<dialog id="input-text">
|
||||
<div class="container">
|
||||
<div class="label">${label}</div>
|
||||
<input id="input-text-el" type="text" placeholder=${placeholder} />
|
||||
<div class="buttons">
|
||||
<div></div>
|
||||
<button onclick=${()=>this.cancel()}>Cancel</button>
|
||||
<button onclick=${()=>this.ok()}>OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
`
|
||||
}//}}}
|
||||
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`<div class="label" style="cursor: pointer" ref=${this.label} onclick=${() => this.editLabel()}>${group.Label}</div>`
|
||||
let addItem = () => {
|
||||
if (this.addingItem.value)
|
||||
return html`<${InputElement} label="New item" callback=${(ok, val) => this.addItem(ok, val)} />`
|
||||
}
|
||||
|
||||
return html`
|
||||
<${addItem} />
|
||||
<div class="checklist-group-container">
|
||||
<div class="checklist-group ${ui.edit.value ? 'edit' : ''}">
|
||||
<div class="reorder" style="cursor: grab">☰</div>
|
||||
<img src="/images/${_VERSION}/trashcan.svg" onclick=${() => this.delete()} />
|
||||
<${label} />
|
||||
<img src="/images/${_VERSION}/add-gray.svg" onclick=${() => this.addingItem.value = true} />
|
||||
</div>
|
||||
<${items} ui=${ui} group=${group} />
|
||||
</div>
|
||||
`
|
||||
}//}}}
|
||||
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`<label ref=${this.label} onclick=${() => this.editLabel()} style="cursor: pointer">${item.Label}</label>`
|
||||
else
|
||||
return html`
|
||||
<input type="checkbox" ref=${this.checkbox} key="checkbox-${item.ID}" id="checkbox-${item.ID}" checked=${checked} onchange=${evt => this.update(evt.target.checked)} />
|
||||
<label ref=${this.label} for="checkbox-${item.ID}">${item.Label}</label>
|
||||
`
|
||||
}
|
||||
return html`
|
||||
<div class="checklist-item ${checked ? 'checked' : ''} ${ui.edit.value ? 'edit' : ''} ${dragTarget ? 'drag-target' : ''}" draggable=true>
|
||||
<div class="reorder" style="user-select: none;">☰</div>
|
||||
<img src="/images/${_VERSION}/trashcan.svg" onclick=${() => this.delete()} />
|
||||
<${checkbox} />
|
||||
</div>
|
||||
`
|
||||
}//}}}
|
||||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue