401 lines
10 KiB
JavaScript
401 lines
10 KiB
JavaScript
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,
|
|
}
|
|
window._checklist = this
|
|
}//}}}
|
|
render({ ui, groups }, { confirmDeletion }) {//{{{
|
|
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>
|
|
`
|
|
}
|
|
|
|
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 ChecklistGroupElement extends Component {
|
|
constructor() {//{{{
|
|
super()
|
|
this.label = createRef()
|
|
}//}}}
|
|
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>`
|
|
|
|
return html`
|
|
<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.addItem()} />
|
|
</div>
|
|
<${items} ui=${ui} group=${group} />
|
|
</div>
|
|
`
|
|
}//}}}
|
|
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 {
|
|
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', () => 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')
|
|
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() {//{{{
|
|
// 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() {//{{{
|
|
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, ()=>console.log('ok'))
|
|
}//}}}
|
|
}
|
|
|
|
// vim: foldmethod=marker
|