diff options
Diffstat (limited to 'html')
-rw-r--r-- | html/dialog.html | 4 | ||||
-rw-r--r-- | html/dialog.js | 33 | ||||
-rw-r--r-- | html/grid.css | 5 | ||||
-rw-r--r-- | html/item_list.html | 39 | ||||
-rw-r--r-- | html/item_list.js | 2 | ||||
-rw-r--r-- | html/item_preview.html | 8 | ||||
-rw-r--r-- | html/item_preview.js | 4 | ||||
-rw-r--r-- | html/payload_create.html | 76 | ||||
-rw-r--r-- | html/payload_create.js | 229 |
9 files changed, 374 insertions, 26 deletions
diff --git a/html/dialog.html b/html/dialog.html index d4e69b9..090a76b 100644 --- a/html/dialog.html +++ b/html/dialog.html @@ -53,12 +53,12 @@ margin-bottom: 2em; text-align: center; } - .dialog_main_div { + .dialog_main_view { margin: 1.4em; } </style> <template> - <div id="dialog" data-template="main_div" class="dialog_main_div"> + <div id="dialog" data-template="main_div" class="dialog_main_view"> <div data-template="msg" class="dialog_msg"></div> <div data-template="ask_buts" class="dialog_buts"> <button data-template="yes_but" class="right_space">Yes</button> diff --git a/html/dialog.js b/html/dialog.js index 6345b2d..22e8aa9 100644 --- a/html/dialog.js +++ b/html/dialog.js @@ -42,7 +42,7 @@ */ #FROM html/DOM_helpers.js IMPORT clone_template -#FROM common/lock.js IMPORT make_lock, lock, unlock +#FROM common/lock.js IMPORT make_lock, lock, try_lock, unlock function make(on_dialog_show, on_dialog_hide) { @@ -68,9 +68,15 @@ function make(on_dialog_show, on_dialog_hide) function close_dialog(dialog_context, event) { + if (event && event.target.parentElement.classList.contains("hide")) + return; + + const result = event ? event.target.haketilo_dialog_result : undefined; + if (dialog_context.queue > 0) - dialog_context.callback(event.target.haketilo_dialog_result); + dialog_context.callback(result); } +#EXPORT close_dialog AS close async function show_dialog(dialog_context, shown_buts_id, msg) { @@ -81,11 +87,19 @@ async function show_dialog(dialog_context, shown_buts_id, msg) dialog_context.on_dialog_show(); } - await lock(dialog_context.lock); - - dialog_context.msg.innerText = msg; + /* + * We want the dialog to be ready for calling close() right after + * show_dialog() gets called. For this, we want locking to happen + * synchronously if possible. If impossible (lock taken), this means dialog + * is already open, hence it's also ready for close()'ing. + */ + if (!try_lock(dialog_context.lock)) + await lock(dialog_context.lock); + + [...dialog_context.msg.childNodes].forEach(n => n.remove()); + dialog_context.msg.append(...msg); for (const buts_id of ["ask_buts", "conf_buts"]) { - const action = buts_id == shown_buts_id ? "remove" : "add"; + const action = buts_id === shown_buts_id ? "remove" : "add"; dialog_context[buts_id].classList[action]("hide"); } @@ -101,12 +115,15 @@ async function show_dialog(dialog_context, shown_buts_id, msg) return result; } -const error = (ctx, msg) => show_dialog(ctx, "conf_buts", msg); +const error = (ctx, ...msg) => show_dialog(ctx, "conf_buts", msg); #EXPORT error /* info() and error() are the same for now, we might later change that. */ const info = error; #EXPORT info -const ask = (ctx, msg) => show_dialog(ctx, "ask_buts", msg); +const ask = (ctx, ...msg) => show_dialog(ctx, "ask_buts", msg); #EXPORT ask + +const loader = (ctx, ...msg) => show_dialog(ctx, null, msg); +#EXPORT loader diff --git a/html/grid.css b/html/grid.css index 59b5bb7..591772e 100644 --- a/html/grid.css +++ b/html/grid.css @@ -27,6 +27,11 @@ * proprietary program, I am not going to enforce this in court. */ +.grid_1 { + display: grid; + grid-template-columns: auto; +} + .grid_2 { display: grid; grid-template-columns: 1fr 1fr; diff --git a/html/item_list.html b/html/item_list.html index d082c5d..5d2a163 100644 --- a/html/item_list.html +++ b/html/item_list.html @@ -41,20 +41,41 @@ #LOADCSS html/base.css #LOADCSS html/grid.css <style> + .item_list { + overflow-y: scroll; + } + .item_list>li { + cursor: pointer; + padding: 0.15em 0.3em; + } + .list_main_view { + grid-template-columns: auto 1fr; + } .item_li_highlight { background-color: #c0c0c0; } + .item_list>li.item_li_highlight { + cursor: default; + } + .list_buttons { + margin: 1em auto; + text-align: center; + } </style> <template> - <div id="item_list" data-template="main_div" class="grid_2"> - <ul data-template="ul"></ul> - <div data-template="preview_container" class="hide"> - <!-- preview div will be dynamically inserted here --> - - <button data-template="remove_but">Remove</button> - <!--<button data-template="export_but">Export</button>--> - </div> - <div data-template="dialog_container" class="hide"> + <div id="item_list" data-template="main_div" class="grid_2 list_main_view"> + <ul data-template="ul" class="item_list"></ul> + <div> + <div data-template="preview_container" class="hide"> + <!-- preview div will be dynamically inserted here --> + <div class="list_buttons"> + <button data-template="remove_but">Remove</button> + <!--<button data-template="export_but">Export</button>--> + </div> + </div> + <div data-template="dialog_container" class="hide"> + <!-- dialog div will be dynamically inserted here --> + </div> </div> </div> </template> diff --git a/html/item_list.js b/html/item_list.js index 55e54fb..34dec83 100644 --- a/html/item_list.js +++ b/html/item_list.js @@ -185,7 +185,7 @@ async function item_list(preview_cb, track_cb, remove_cb) function on_dialog_show(list_ctx) { - list_ctx.ul; + list_ctx.ul; // TODO: make ul non-selectable when dialog is shown list_ctx.preview_container.classList.add("hide"); list_ctx.dialog_container.classList.remove("hide"); } diff --git a/html/item_preview.html b/html/item_preview.html index 76c6da6..a00d299 100644 --- a/html/item_preview.html +++ b/html/item_preview.html @@ -38,12 +38,12 @@ #LOADCSS html/base.css #LOADCSS html/grid.css <style> - .dialog_main_div { - margin: 1.4em; + .preview_main_div { + margin: 0.8em 0; } </style> <template> - <div id="resource_preview_div" data-template="main_div" + <div id="resource_preview" data-template="main_div" class="grid_2 grid_form preview_main_div"> <h3 class="grid_col_both">resource preview</h3> <span class="grid_col_1">identifier:</span> @@ -65,7 +65,7 @@ <span class="grid_col_1">copyright:</span> <span class="grid_col_2"><ul data-template="copyright"></ul></span> </div> - <div id="mapping_preview_div" data-template="main_div" + <div id="mapping_preview" data-template="main_div" class="grid_2 grid_form"> <h3 class="grid_col_both">mapping preview</h3> <span class="grid_col_1">identifier:</span> diff --git a/html/item_preview.js b/html/item_preview.js index 447b16a..474766c 100644 --- a/html/item_preview.js +++ b/html/item_preview.js @@ -85,7 +85,7 @@ function make_file_link(preview_object, file_ref) function resource_preview(resource, preview_object, dialog_context) { if (preview_object === undefined) - preview_object = clone_template("resource_preview_div"); + preview_object = clone_template("resource_preview"); preview_object.identifier.innerText = resource.identifier; preview_object.long_name.innerText = resource.long_name; @@ -116,7 +116,7 @@ function resource_preview(resource, preview_object, dialog_context) function mapping_preview(mapping, preview_object, dialog_context) { if (preview_object === undefined) - preview_object = clone_template("mapping_preview_div"); + preview_object = clone_template("mapping_preview"); preview_object.identifier.innerText = mapping.identifier; preview_object.long_name.innerText = mapping.long_name; diff --git a/html/payload_create.html b/html/payload_create.html new file mode 100644 index 0000000..0f6927f --- /dev/null +++ b/html/payload_create.html @@ -0,0 +1,76 @@ +#IF !PAYLOAD_CREATE_LOADED +#DEFINE PAYLOAD_CREATE_LOADED +<!-- + SPDX-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 + + Simple site payload creation form. + + This file is part of Haketilo. + + Copyright (C) 2022 Wojtek Kosior <koszko@koszko.org> + + File is dual-licensed. You can choose either GPLv3+, CC BY-SA or both. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + + I, Wojtek Kosior, thereby promise not to sue for violation of this file's + licenses. Although I request that you do not make use of this code in a + proprietary program, I am not going to enforce this in court. + --> + +<!-- + This is not a standalone page. This file is meant to be imported into other + HTML code. + --> + +#INCLUDE html/dialog.html + +#LOADCSS html/reset.css +#LOADCSS html/base.css +#LOADCSS html/grid.css +<style> + .payload_create_main_view { + margin: 0.8em; + } + .payload_create_buts { + text-align: center; + } +</style> +<template> + <div id="payload_create" data-template="main_div" + class="payload_create_main_view"> + <div data-template="form_container" class="grid_1"> + <h3>create_payload</h3> + <label> + identifier* (may only contain digits 0-9, lowercase letters a-z and hyphens '-'; will have 'local-' prepended): + </label> + <input data-template="identifier"> + <label>long name (defaults to the same as identifier):</label> + <input data-template="long_name"> + <label>description:</label> + <textarea data-template="description"></textarea> + <label>URL patterns* (put each on its own line):</label> + <textarea data-template="patterns">https://example.com/***</textarea> + <label>script to inject*:</label> + <textarea data-template="script">console.log("Hello, World!");</textarea> + <div class="payload_create_buts"> + <button data-template="create_but">Create</button> + </div> + </div> + <div data-template="dialog_container" class="hide"> + <!-- dialog div will be dynamically inserted here --> + </div> + </div> +</template> +#ENDIF diff --git a/html/payload_create.js b/html/payload_create.js new file mode 100644 index 0000000..db63a82 --- /dev/null +++ b/html/payload_create.js @@ -0,0 +1,229 @@ +/** + * This file is part of Haketilo. + * + * Function: Driving the site payload creation form. + * + * Copyright (C) 2022 Wojtek Kosior + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * As additional permission under GNU GPL version 3 section 7, you + * may distribute forms of that code without the copy of the GNU + * GPL normally required by section 4, provided you include this + * license notice and, in case of non-source distribution, a URL + * through which recipients can access the Corresponding Source. + * If you modify file(s) with this exception, you may extend this + * exception to your version of the file(s), but you are not + * obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + * + * As a special exception to the GPL, any HTML file which merely + * makes function calls to this code, and for that purpose + * includes it by reference shall be deemed a separate work for + * copyright law purposes. If you modify this code, you may extend + * this exception to your version of the code, but you are not + * obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * I, Wojtek Kosior, thereby promise not to sue for violation of this file's + * license. Although I request that you do not make use of this code in a + * proprietary program, I am not going to enforce this in court. + */ + +#IMPORT html/dialog.js +#IMPORT common/indexeddb.js AS haketilodb + +#FROM html/DOM_helpers.js IMPORT clone_template +#FROM common/sha256.js IMPORT sha256 +#FROM common/patterns.js IMPORT deconstruct_url + +/* https://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid */ +/* This is a helper function used by uuidv4(). */ +function uuid_replace_num(num) +{ + const randbyte = crypto.getRandomValues(new Uint8Array(1))[0]; + return (num ^ randbyte & 15 >> num / 4).toString(16); +} + +/* Generate a new UUID. */ +function uuidv4() +{ + return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, uuid_replace_num); +} + +function collect_form_data(form_ctx) +{ + const identifier_nonprepended = form_ctx.identifier.value; + if (!identifier_nonprepended) + throw "The 'identifier' field is required!"; + if (!/[-a-zA-Z]+/.test(identifier_nonprepended)) + throw "Identifier may only contain digits 0-9, lowercase letters a-z and hyphens '-'!" + const identifier = `local-${identifier_nonprepended}`; + + const long_name = form_ctx.long_name.value || identifier_nonprepended; + + const description = form_ctx.description.value; + + const url_patterns = form_ctx.patterns.value.split("\n").filter(i => i); + if (url_patterns.length === 0) + throw "The 'URL patterns' field is required!"; + + const payloads = {}; + + for (const pattern of url_patterns) { + try { + deconstruct_url(pattern); + } catch(e) { + const patterns_doc_url = + "https://hydrillabugs.koszko.org/projects/haketilo/wiki/URL_patterns"; + const patterns_doc_link = document.createElement("a"); + patterns_doc_link.href = patterns_doc_url; + patterns_doc_link.innerText = "here"; + const msg = document.createElement("span"); + msg.prepend(`'${pattern}' is not a valid URL pattern. See `, + patterns_doc_link, " for more details."); + throw msg; + } + + if (pattern in payloads) + throw `Pattern '${pattern}' soecified multiple times!`; + + payloads[pattern] = {identifier}; + } + + const script = form_ctx.script.value; + if (!script) + throw "The 'script' field is required!"; + const hash_key = `sha256-${sha256(script)}`; + + const resource = { + source_name: identifier, + source_copyright: [], + type: "resource", + identifier, + long_name, + uuid: uuidv4(), + version: [1], + description, + dependencies: [], + scripts: [{file: "payload.js", hash_key}] + }; + + const mapping = { + source_name: identifier, + source_copyright: [], + type: "mapping", + identifier, + long_name, + uuid: uuidv4(), + version: [1], + description, + payloads + }; + + return {identifier, resource, mapping, files: {[hash_key]: script}}; +} + +async function save_payload(saving) +{ + const db = await haketilodb.get(); + const tx_starter = haketilodb.start_items_transaction; + const tx_ctx = await tx_starter(["resources", "mappings"], saving.files); + + for (const [type, store_name] of + [["resource", "resources"], ["mapping", "mappings"]]) { + if (!saving[`override_${type}`] && + (await haketilodb.idb_get(tx_ctx.transaction, store_name, + saving.identifier))) { + saving.ask_override = "resource"; + return; + } + } + + await haketilodb.save_item(saving.resource, tx_ctx); + await haketilodb.save_item(saving.mapping, tx_ctx); + + return haketilodb.finalize_transaction(tx_ctx); +} + +function override_question(saving) +{ + return saving.ask_override === "resource" ? + `Resource '${saving.identifier}' alredy exists. Override?` : + `Mapping '${saving.identifier}' alredy exists. Override?`; +} + +async function create_clicked(form_ctx) +{ + if (form_ctx.dialog_ctx.shown) + return; + + dialog.loader(form_ctx.dialog_ctx, "Saving payload..."); + + try { + var saving = collect_form_data(form_ctx); + } catch(e) { + dialog.error(form_ctx.dialog_ctx, e); + dialog.close(form_ctx.dialog_ctx); + return; + } + + try { + do { + if (saving.ask_override) { + const override_prom = dialog.ask(form_ctx.dialog_ctx, + override_question(saving)); + dialog.loader(form_ctx.dialog_ctx, "Saving payload..."); + dialog.close(form_ctx.dialog_ctx); + if (!(await override_prom)) + throw "Saving would override existing data."; + + saving[`override_${saving.ask_override}`] = true; + delete saving.ask_override; + } + + await save_payload(saving); + } while (saving.ask_override); + + dialog.info(form_ctx.dialog_ctx, "Successfully saved payload!"); + } catch(e) { + console.error(e); + dialog.error(form_ctx.dialog_ctx, "Failed to save payload :(") + } + + dialog.close(form_ctx.dialog_ctx); +} + +function on_show_hide(form_ctx, what_to_show) +{ + for (const item_id of ["form", "dialog"]) { + const action = item_id === what_to_show ? "remove" : "add"; + form_ctx[`${item_id}_container`].classList[action]("hide"); + } +} + +function payload_create_form() +{ + const form_ctx = clone_template("payload_create"); + + form_ctx.dialog_ctx = dialog.make(() => on_show_hide(form_ctx, "dialog"), + () => on_show_hide(form_ctx, "form")); + form_ctx.dialog_container.prepend(form_ctx.dialog_ctx.main_div); + + form_ctx.create_but.addEventListener("click", + () => create_clicked(form_ctx)); + + return form_ctx; +} +#EXPORT payload_create_form |