// SPDX-FileCopyrightText: 2023 Vasilij Schneidermann // // SPDX-License-Identifier: GPL-3.0-or-later 'use strict'; let modo = { createElement: function(tag, attrs, ...children) { let el = document.createElement(tag); Object.entries(attrs).forEach(function(attr) { el.setAttribute(attr[0], attr[1]); }); el.append(...children); return el; }, createListItem: function(json) { const h = this.createElement; const lid = json.lid.toString(); const content = json.content; return h('div', {'class': 'modo-list-item card my-3'}, h('div', {'class': 'card-header d-flex flex-row align-items-center'}, h('span', {'class': 'modo-list-drag-handle'}, h('i', {'class': 'fa-solid fa-arrows-up-down'})), h('span', {'class': 'content me-auto'}, content), h('button', {'class': 'btn', 'name': 'edit', 'data-id': lid, 'onclick': 'modo.editList(event)'}, h('i', {'class': 'fa-solid fa-pencil'})), h('button', {'class': 'btn btn-danger', 'name': 'delete', 'data-id': lid, 'onclick': 'modo.deleteList(event)'}, h('i', {'class': 'fa-solid fa-trash'}))), h('div', {'class': 'modo-todo-container list-group'}, h('div', {'class': 'modo-todo-new modo-todo-item list-group-item'}, h('div', {'class': 'd-flex flex-row'}, h('input', {'class': 'description px-1 me-auto', 'placeholder': 'New todo', 'oninput': 'modo.updateCreateTodoButton(event)'}), h('button', {'class': 'btn btn-secondary', 'disabled': '', 'data-id': lid, 'onclick': 'modo.createTodo(event)'}, h('i', {'class': 'fa-solid fa-file-circle-plus'})))), h('div', {'class': 'modo-todo-items'}))); }, createTodoItem: function(json) { const h = this.createElement; const lid = json.lid.toString(); const tid = json.tid.toString(); const content = json.content; return h('div', {'class': 'modo-todo-item list-group-item'}, h('div', {'class': 'd-flex flex-row align-items-center'}, h('span', {'class': 'modo-todo-drag-handle'}, h('i', {'class': 'fa-solid fa-arrows-up-down'})), h('div', {'class': 'form-check form-check-inline'}, h('input', {'class': 'form-check-input', 'type': 'checkbox', 'data-id': tid, 'data-lid': lid, 'onchange': 'modo.toggleTodo(event)'})), h('span', {'class': 'content me-auto'}, content), h('button', {'class': 'btn', 'name': 'edit', 'data-id': tid, 'data-lid': lid, 'onclick': 'modo.editTodo(event)'}, h('i', {'class': 'fa-solid fa-pencil'})), h('button', {'class': 'btn btn-danger', 'name': 'delete', 'data-id': tid, 'data-lid': lid, 'onclick': 'modo.deleteTodo(event)'}, h('i', {'class': 'fa-solid fa-trash'})))); }, apiRequest: function(method, path, json, success, fail) { let logMessage = `${method} ${path}`; if (json !== null) { logMessage += ` ${JSON.stringify(json)}`; } console.log(logMessage); let req = new XMLHttpRequest(); req.addEventListener('loadend', function() { if (success !== null && req.status < 400) { success(req); } else if (fail !== null && req.status >= 400) { fail(req); } }); req.open(method, path); if (json !== null) { req.setRequestHeader('Content-Type', 'application/json'); req.send(JSON.stringify(json)); } else { req.send(); } }, createList: function(e) { e.preventDefault(); e.stopPropagation(); let that = this; let button = e.currentTarget; let textinput = button.parentElement.querySelector('input.description'); const content = textinput.value; const url = '/api/list'; const json = { 'content': content }; if (content !== '') { that.apiRequest('POST', url, json, function(xhr) { let lists = document.querySelector('.modo-list-items'); let listJson = JSON.parse(xhr.response); const url = `/api/list/${listJson.lid}/reorder`; const json = { 'to': 1 } that.apiRequest('PUT', url, json, function(xhr) { let list = that.createListItem(listJson); lists.prepend(list); Sortable.create(list.querySelector('.modo-todo-items'), { 'handle': '.modo-todo-drag-handle', 'onEnd': that.moveTodo.bind(that) }); textinput.value = ''; button.disabled = true; }, function(xhr) { that.showError('reorder to 1 failed'); }); }, function(xhr) { that.showError('createList failed'); }); } }, updateCreateListButton: function(e) { let textinput = e.currentTarget; let button = textinput.parentElement.querySelector('button'); button.disabled = (textinput.value === ''); }, moveList: function(e) { let that = this; const lid = e.item.dataset.id; const to = e.newIndex + 1; const url = `/api/list/${lid}/reorder`; const json = { 'to': to }; that.apiRequest('PUT', url, json, null, function(xhr) { that.showError('moveList reorder failed'); }); }, editList: function(e) { e.preventDefault(); e.stopPropagation(); let that = this; let button = e.currentTarget; let label = button.parentElement.querySelector('span.content'); const lid = button.dataset.id; const content = prompt('Please enter new name', label.textContent); if (content !== null && content !== '') { const url = `/api/list/${lid}`; const json = { 'content': content }; that.apiRequest('PUT', url, json, function(xhr) { label.textContent = content; }, function(xhr) { that.showError('editList failed'); }); } }, deleteList: function(e) { e.preventDefault(); e.stopPropagation(); let that = this; let button = e.currentTarget; const lid = button.dataset.id; const url = `/api/list/${lid}`; if (confirm('Are you sure?')) { // TODO: show error popup for non-empty list that.apiRequest('DELETE', url, null, function(xhr) { let list = button.parentElement.parentElement; list.remove(); }, function(xhr) { that.showError('deleteList failed'); }); } }, createTodo: function(e) { e.preventDefault(); e.stopPropagation(); let that = this; let button = e.currentTarget; const lid = button.dataset.id; let textinput = button.parentElement.querySelector('input.description'); const content = textinput.value; const url = `/api/list/${lid}/todo`; const json = { 'content': content }; if (content !== '') { let todoContainer = button.parentElement.parentElement.parentElement; let todoList = todoContainer.querySelector('.modo-todo-items'); that.apiRequest('POST', url, json, function(xhr) { let todoJson = JSON.parse(xhr.response); const url = `/api/list/${todoJson.lid}/todo/${todoJson.tid}/reorder`; const json = { 'to': 1 }; that.apiRequest('PUT', url, json, function(xhr) { let todo = that.createTodoItem(todoJson); todoList.prepend(todo) textinput.value = ''; button.disabled = true; }, function(xhr) { that.showError('reorder to 1 failed'); }); }, function(xhr) { that.showError('createTodo failed'); }); } }, updateCreateTodoButton: function(e) { let textinput = e.currentTarget; let button = textinput.parentElement.querySelector('button'); button.disabled = (textinput.value === ''); }, moveTodo: function(e) { let that = this; const lid = e.item.dataset.lid; const tid = e.item.dataset.id; const to = e.newIndex + 1; const url = `/api/list/${lid}/todo/${tid}/reorder`; const json = { 'to': to }; that.apiRequest('PUT', url, json, null, function(xhr) { that.showError('moveTodo reorder failed'); }); }, reorderTodoTop: function(todoElement) { let todoElements = todoElement.parentElement; if (todoElements.children[0] != todoElement) { todoElement.remove(); todoElements.prepend(todoElement); } }, reorderTodoBottom: function(todoElement) { let todoElements = todoElement.parentElement; const lastChildIndex = todoElements.children.length - 1; if (todoElements.children[lastChildIndex] != todoElement) { todoElement.remove(); todoElements.append(todoElement); } }, toggleTodo: function(e) { e.preventDefault(); e.stopPropagation(); let that = this; let checkbox = e.currentTarget; const lid = checkbox.dataset.lid; const tid = checkbox.dataset.id; const checked = checkbox.checked; const action = checked ? 'finish' : 'resurrect'; const url = `/api/list/${lid}/todo/${tid}/${action}`; that.apiRequest('PUT', url, null, function(xhr) { const url = `/api/list/${lid}/todo/${tid}/reorder`; const to = checked ? -1 : 1; const json = { 'to': to }; that.apiRequest('PUT', url, json, function(xhr) { let todoElement = checkbox.parentElement.parentElement.parentElement; if (checked) { that.reorderTodoBottom(todoElement); } else { that.reorderTodoTop(todoElement); } }, function(xhr) { that.showError('toggleTodo reorder failed'); }); }, function(xhr) { that.showError('toggleTodo failed'); }); }, editTodo: function(e) { e.preventDefault(); e.stopPropagation(); let that = this; let button = e.currentTarget; let label = button.parentElement.querySelector('span.content'); const lid = button.dataset.lid; const tid = button.dataset.id; const url = `/api/list/${lid}/todo/${tid}` const content = prompt('Please enter new name', label.textContent); if (content !== null && content !== '') { const json = { 'content': content }; that.apiRequest('PUT', url, json, function(xhr) { label.textContent = content; }, function(xhr) { that.showError('editTodo failed'); }); } }, deleteTodo: function(e) { e.preventDefault(); e.stopPropagation(); let that = this; let button = e.currentTarget; const lid = button.dataset.lid; const tid = button.dataset.id; const url = `/api/list/${lid}/todo/${tid}` if (confirm('Are you sure?')) { that.apiRequest('DELETE', url, null, function(xhr) { let todo = button.parentElement.parentElement; todo.remove(); }, function(xhr) { that.showError('deleteTodo failed'); }); } }, showDoneItems: true, toggleDoneItemsDisplay: function(e) { if (this.showDoneItems) { console.log("Turning display off..."); } else { console.log("Turning display on..."); } document.querySelectorAll('.modo-todo-item').forEach(function(item) { if (item.querySelector('.form-check input[checked=""]')) { item.classList.toggle('hidden'); } }); this.showDoneItems = !this.showDoneItems; }, errorToastInitialized: false, errorToastElement: null, errorToast: null, initializeErrorToast: function() { if (!this.errorToastInitialized) { let errorToastElement = document.querySelector('#errorToast'); let errorToast = bootstrap.Toast.getOrCreateInstance(errorToastElement); this.errorToastElement = errorToastElement; this.errorToast = errorToast; this.errorToastInitialized = true; } }, showError: function(message) { this.errorToastElement.querySelector('.toast-body').textContent = message; this.errorToast.show(); }, initializeSortable: function() { let that = this; Sortable.create(document.querySelector('.modo-list-items'), { 'handle': '.modo-list-drag-handle', 'onEnd': that.moveList.bind(that) }); document.querySelectorAll('.modo-todo-items').forEach(function(item) { Sortable.create(item, { 'handle': '.modo-todo-drag-handle', 'onEnd': that.moveTodo.bind(that) }); }); }, } document.addEventListener('DOMContentLoaded', function() { modo.initializeErrorToast(); modo.initializeSortable(); });