aboutsummaryrefslogtreecommitdiff
path: root/html
diff options
context:
space:
mode:
authorWojtek Kosior <koszko@koszko.org>2022-01-10 23:38:56 +0100
committerWojtek Kosior <koszko@koszko.org>2022-01-10 23:38:56 +0100
commit19304cd1ae4e4ba4f6dcf4f1db14de1e4e70c250 (patch)
tree2e7e6f904ad16f9402827a7bc215a419de5c2656 /html
parent38650a8102fe0841617cd80f3a6e45b1f5f62fd5 (diff)
downloadbrowser-extension-19304cd1ae4e4ba4f6dcf4f1db14de1e4e70c250.tar.gz
browser-extension-19304cd1ae4e4ba4f6dcf4f1db14de1e4e70c250.zip
improve item list styling; add payload creation form; exend dialog mechanism
Diffstat (limited to 'html')
-rw-r--r--html/dialog.html4
-rw-r--r--html/dialog.js33
-rw-r--r--html/grid.css5
-rw-r--r--html/item_list.html39
-rw-r--r--html/item_list.js2
-rw-r--r--html/item_preview.html8
-rw-r--r--html/item_preview.js4
-rw-r--r--html/payload_create.html76
-rw-r--r--html/payload_create.js229
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