Files
jsl/jsonform.js
Christopher Vagnetoft bd7b7ac498 Bugfixes, dialog improvements
* It is now possible to pass an element as the dialog message to have it inserted as the body.
2025-09-21 00:45:34 +02:00

295 lines
6.4 KiB
JavaScript

import { dom, el } from './dom.js';
import { jsonQuery, jsonPatch } from './json.js';
/**
* This form encapsulates a form with a model and a layout.
*
*/
class JsonForm {
#dom = null;
#model = null;
#layout = null;
#editable = false;
set model(model) {
this.#model = model;
this.#layout.modelUpdated(model);
}
get model() {
return this.#model;
}
set editable(state) {
this.#editable = !!state;
this.#layout.setEditable(this.#editable);
if (this.#editable === false) {
this.refreshModel();
}
}
get editable() {
return this.#editable;
}
get layout() {
return this.#layout;
}
constructor() {
this.#layout = new FormLayout();
}
dom() {
if (!this.#dom) {
this.#dom = el.div({ class: 'jsl-form' });
}
this.#layout.buildDom(this.#dom);
return this.#dom;
}
refreshModel() {
// TODO go over the fields and update the model with any modified data
this.#model = this.#layout.updateModel(this.#model);
}
}
class FormLayout {
#rows = [];
addRow() {
const row = new FormRow();
this.#rows.push(row);
return row;
}
buildDom(element) {
element.innerHTML = '';
this.#rows.forEach(row => {
const rowEl = row.dom();
element.appendChild(rowEl);
})
}
modelUpdated(model) {
this.#rows.forEach(row => row.modelUpdated(model));
}
updateModel(model) {
this.#rows.forEach(row => model = row.updateModel(model));
return model;
}
setEditable(state) {
this.#rows.forEach(row => row.setEditable(state));
}
}
class FormRow {
el = null;
#fields = [];
append(field) {
this.#fields.push(field);
return this;
}
dom() {
if (!this.el) {
this.el = el.div({ class: '-row' });
}
this.el.innerHTML = '';
this.#fields.forEach(field => {
this.el.appendChild(field.dom());
})
return this.el;
}
modelUpdated(model) {
this.#fields.forEach(field => {
if (!field.options.path) return;
field.value = jsonQuery(model, field.options.path);
});
}
updateModel(model) {
this.#fields.forEach(field => {
if (!field.options.path) return;
field.value = field.getValue();
model = jsonPatch(model, field.options.path, field.value);
});
return model;
}
setEditable(state) {
this.#fields.forEach(field => {
if (field.options.locked === true) return;
field.setEditable(state)
});
}
}
class FormField {
#el = null;
#label = null;
#options = {};
#value = null;
get value() {
return this.#value;
}
set value(val) {
this.#value = val;
this.valueUpdated();
}
get el() {
return this.#el;
}
get options() {
return this.#options;
}
constructor(options = {}) {
this.#options = options || {};
}
dom() {
if (!this.#el) {
let style = null;
if (this.#options.width) {
style = 'width: ' + this.#options.width + '%;';
} else {
style = 'flex-grow: 1;';
}
this.#el = el.div({ class: '-form-field', style: style ?? null });
this.#label = el.label({ class: '-label' });
this.#label.innerHTML = this.options.label ?? "Unlabeled";
this.#el.appendChild(this.#label);
}
this.buildField();
return this.#el;
}
/**
* Update the value without triggering any effects
*
* @param {*} value The new value
*/
updateValue(value) {
this.#value = value;
}
buildField() {
}
valueUpdated() {
}
setEditable(state) {
}
}
class TextField extends FormField {
#input = null;
buildField() {
if (!this.#input) {
this.#input = el.div({ class: '-field -text-field' })
this.el.appendChild(this.#input);
}
this.#input.innerText = this.value;
}
valueUpdated() {
if (!this.#input) return;
this.#input.innerText = this.value;
}
getValue() {
if (this.#input.innerText === '') {
if (this.options.nullable !== false)
return null;
}
return this.#input.innerText;
}
setEditable(state) {
if (state) {
dom.apply(this.#input, { 'contenteditable':'plaintext-only' });
} else {
this.updateValue(this.#input.innerText);
dom.apply(this.#input, { 'contenteditable':null });
}
}
}
class NumericField extends FormField {
#input = null;
buildField() {
if (!this.#input) {
this.#input = el.div({ class: '-field -number-field' })
this.el.appendChild(this.#input);
}
this.#input.innerText = this.value;
}
valueUpdated() {
if (!this.#input) return;
this.#input.innerText = this.value;
}
getValue() {
if (this.#input.innerText === '') {
if (this.options.nullable !== false)
return null;
}
if (this.options.fixed && typeof this.options.fixed == 'number') {
return parseFloat(this.#input.innerText).toFixed(this.options.fixed);
}
if (this.options.float) {
return parseFloat(this.#input.innerText);
}
return parseInt(this.#input.innerText);
}
setEditable(state) {
if (state) {
dom.apply(this.#input, { 'contenteditable':'plaintext-only' });
} else {
this.updateValue(this.#input.innerText);
dom.apply(this.#input, { 'contenteditable':null });
}
}
}
class DateField extends FormField {
#input = null;
buildField() {
if (!this.#input) {
this.#input = el.div({ class: '-field -date-field' })
this.el.appendChild(this.#input);
}
this.#input.innerHTML = "I'm a date!";
}
}
class SelectField extends FormField {
}
class CheckField extends FormField {
}
export {
JsonForm,
FormLayout,
FormRow,
FormField,
TextField,
NumericField,
DateField,
SelectField,
CheckField,
}