diff --git a/README.md b/README.md index 2d532e1..75b8e83 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,13 @@ dialog.msgbox("Ready!").then( dialog.select("Put in warehouse", "Select", { "none":"None", "1":"WH1", "2":"WH2" }).then(...) ``` +* `dialog.TYPE(body, ?title, ?actions, ?options)` → `Promise` + +Options: + +- `showClose` - true to show close button in top left corner +- `width` - valid CSS width definition + ### dom.js This component offers `dom` and `el` that can be used to manipulate the DOM. @@ -143,4 +150,16 @@ productForm.editable = true; // when the user is done editing set editable = false and access the updated model productForm.editable = false; const newModel = productForm.model; // → { product:{ name:"Swedish Fish" }, price:"8.99" } + +// You can even put a form inside a dialog! +dialog.dialog(productForm.dom(), "Title", {"ok":"Update"}, { showClose:true, width:'400px' }).then( + (result) => { + productForm.editable = false; + if (result == 'ok') { + // productForm.model is updated + } + } +) +productForm.editable = true; + ``` diff --git a/dialog.css b/dialog.css index 34df86c..e1c5106 100644 --- a/dialog.css +++ b/dialog.css @@ -53,7 +53,7 @@ border-top-right-radius: 0.5rem; /* background: rgba(0, 0, 0, 0.2); */ display: flex; - padding: 0.5rem; + padding: 0.75rem; } .-title { @@ -68,18 +68,18 @@ } .-content { - padding: 0.5rem; + padding: 0px 0.75rem; } .-buttons { - padding: 0.5rem; - padding-top: 0.2rem; - text-align: center; + padding: 0.75rem; + text-align: right; .action { + font-size: 105%; border: solid 1px #666; border-radius: 0.25rem; - padding: 0.35rem 0.6rem; + padding: 0.45rem 0.8rem; margin: 0px 0.25rem; background: #eee; diff --git a/dialog.js b/dialog.js index 33fa672..4338710 100644 --- a/dialog.js +++ b/dialog.js @@ -20,6 +20,7 @@ class UserDialog { #title = "MessageBox"; #message; #actions = { "ok":"Ok" }; + #options = {}; /** * Dialog constructor @@ -29,11 +30,12 @@ class UserDialog { * @param {string} message The dialog message * @param {object} actions The dialog actions */ - constructor(style, title, message, actions) { + constructor(style, title, message, actions, options) { if (style) this.#style = style; if (title) this.#title = title; if (message) this.#message = message; if (actions && typeof actions == 'object') this.#actions = actions; + if (options && typeof options == 'object') this.#options = options; } /** @@ -56,6 +58,9 @@ class UserDialog { throw new Error("#buildDialog should not be called with an active #dialog"); const dialog = el.dialog({ class: 'jsl-dialog' }); + if (this.#options.width) { + dialog.style.width = this.#options.width; + } dialog.addEventListener('close', event => { if (this.#resolver) { this.#resolver(false); @@ -68,10 +73,16 @@ class UserDialog { let cbtn; dom.append(dialog, el.div({ class: '-header' }, [ el.div({ class: '-title' }, this.#title), - // cbtn = el.button({ class: '-close-btn' }, '✕') + cbtn = (this.#options.showClose ?? false) ? el.button({ class: '-close-btn' }, '✕') : null, ])); - dom.append(dialog, el.div({ class: '-content' }, this.#message)); - // cbtn.addEventListener('click', () => { this.#handleButton(false); }); + if (this.#options.body ?? null) { + dom.append(dialog, el.div({ class: '-content' }, this.#options.body)); + } else { + dom.append(dialog, el.div({ class: '-content' }, this.#message)); + } + if (this.#options.showClose ?? false) { + cbtn.addEventListener('click', () => { this.#handleButton(false); }); + } const buttons = el.div({ class: '-buttons' }); let tabindex = 1; @@ -129,39 +140,39 @@ class UserDialogFactory { * @param {object} actions The buttons to present at the bottom of the dialog * @returns Promise */ - showDialog(type, title, message, actions) { + showDialog(type, title, message, actions, options) { let dialog; switch (type) { case 'msgbox': title = title ?? "MessageBox"; actions = actions ?? { 'ok': 'Ok' }; - dialog = new UserDialog('msgbox', title, message, actions); + dialog = new UserDialog('msgbox', title, message, actions, options); break; case 'information': case 'info': title = title ?? "Information"; actions = actions ?? { 'ok': 'Ok' }; - dialog = new UserDialog('info', title, message, actions); + dialog = new UserDialog('info', title, message, actions, options); break; case 'error': title = title ?? "Error"; actions = actions ?? { 'ok': 'Ok' }; - dialog = new UserDialog('error', title, message, actions); + dialog = new UserDialog('error', title, message, actions, options); break; case 'warning': title = title ?? "Warning"; actions = actions ?? { 'ok': 'Ok' }; - dialog = new UserDialog('warning', title, message, actions); + dialog = new UserDialog('warning', title, message, actions, options); break; case 'confirm': title = title ?? "Confirmation"; actions = actions ?? { 'ok': 'Ok', 'cancel': 'Cancel' }; - dialog = new UserDialog('confirm', title, message, actions); + dialog = new UserDialog('confirm', title, message, actions, options); break; default: title = title ?? "Dialog"; actions = actions ?? { 'ok': 'Ok' }; - dialog = new UserDialog(type, title, message, actions); + dialog = new UserDialog(type, title, message, actions, options); } return dialog.show(); @@ -176,7 +187,7 @@ const dialog = new Proxy(factory, { get(target, name) { return name in target ? target[name] - : (message, title = null, actions = null) => target.showDialog(name, title, message, actions); + : (message, title = null, actions = null, options = {}) => target.showDialog(name, title, message, actions, options); } }); diff --git a/dom.js b/dom.js index 99f3b7d..c4d1d2c 100644 --- a/dom.js +++ b/dom.js @@ -56,6 +56,8 @@ class DomHelper { * @returns */ append(tag, children) { + // Handle null, to make it easy to skip items with conditionals + if (children === null) return; //console.debug(`append: ${tag.nodeName}`, children); if (typeof children == 'object' && children instanceof Element) { //console.debug(`-> appended element`); diff --git a/index.html b/index.html index b29394e..9224cbf 100644 --- a/index.html +++ b/index.html @@ -15,7 +15,7 @@ import { jsonQuery, jsonPatch, tokenizePath } from './json.js'; import { date } from './date.js'; // build our dom -let btn, btn2, btn3; +let btn, btn2, btn3, btn4; let root = el.div({}, [ el.div({ style:'color:#444;' }, "This is dynamically generated!"), el.div({ style:'display:flex; gap:0.25rem; margin-top:0.25rem;' }, [ @@ -23,6 +23,7 @@ let root = el.div({}, [ btn = el.button({}, "Hello World"), btn2 = el.button({}, "Error mode"), btn3 = el.button({}, "Edit form"), + btn4 = el.button({}, "Form modal"), ]) ]); // click handler @@ -49,6 +50,8 @@ userForm.layout.addRow() userForm.layout.addRow() .append(new TextField({ label:"Created", width:50, path:".meta.created", locked:true })) .append(new TextField({ label:"Updated", width:50, path:".meta.updated", locked:true })); +userForm.layout.addRow() + .append(new NumericField({ label:"Salary", width:30, path:".meta.salary", fixed:2 })); document.body.appendChild(userForm.dom()); // Set the model @@ -67,6 +70,26 @@ btn3.addEventListener('click', () => { } }); +const productForm = new JsonForm(); +productForm.layout.addRow() + .append(new TextField({ label:"Product", width:60, path:".product.name" })) + .append(new NumericField({ label:"Price", path:".product.price", fixed:2 })); + +productForm.model = {}; + +btn4.addEventListener('click', () => { + dialog.dialog(productForm.dom(), "Title", {"ok":"Update"}, { showClose:true, width:'400px' }).then( + (result) => { + productForm.editable = false; + if (result == 'ok') { + console.log(productForm.model); + } + } + ) + productForm.editable = true; +}); + + // console.log(tokenizePath(".foo.bar[1].title")); // console.log(jsonQuery({ "foo":{ "bar":"42" }}, ".foo.bar" )); // console.log(jsonQuery([ { name:"cheese" }, { name:"bread" }, { name:"milk" } ], "[].name" )); diff --git a/json.js b/json.js index 066c595..e53821c 100644 --- a/json.js +++ b/json.js @@ -90,7 +90,13 @@ function jsonPatch(json, path, value) { if (search === 'key') { // find key tok in ptr if (typeof ptr[tok] === 'undefined') { - return json; + if (toks.length === 0) { + ptr[tok] = value; + return json; + } else { + ptr[tok] = {}; + ptr = ptr[tok]; + } } else { if (toks.length === 0) { ptr[tok] = value; diff --git a/jsonform.js b/jsonform.js index f53089b..9de07d0 100644 --- a/jsonform.js +++ b/jsonform.js @@ -119,6 +119,7 @@ class FormRow { 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; @@ -206,9 +207,14 @@ class TextField extends FormField { 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) { @@ -231,9 +237,17 @@ class NumericField extends FormField { 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); }