/**
* Hachette HTML options page main script
*
* Copyright (C) 2021 Wojtek Kosior
* Redistribution terms are gathered in the `copyright' file.
*/
/*
* IMPORTS_START
* IMPORT get_remote_storage
* IMPORT TYPE_PREFIX
* IMPORT TYPE_NAME
* IMPORT list_prefixes
* IMPORT nice_name
* IMPORT parse_json_with_schema
* IMPORTS_END
*/
var storage;
function by_id(id)
{
return document.getElementById(id);
}
const item_li_template = by_id("item_li_template");
const bag_component_li_template = by_id("bag_component_li_template");
const chbx_component_li_template = by_id("chbx_component_li_template");
const radio_component_li_template = by_id("radio_component_li_template");
const import_li_template = by_id("import_li_template");
/* Make sure they are later cloned without id. */
item_li_template.removeAttribute("id");
bag_component_li_template.removeAttribute("id");
chbx_component_li_template.removeAttribute("id");
radio_component_li_template.removeAttribute("id");
import_li_template.removeAttribute("id");
function item_li_id(prefix, item)
{
return `li_${prefix}_${item}`;
}
/* Insert into list of bags/pages/scripts/repos */
function add_li(prefix, item, at_the_end=false)
{
let ul = ul_by_prefix[prefix];
let li = item_li_template.cloneNode(true);
li.id = item_li_id(prefix, item);
let span = li.firstElementChild;
span.textContent = item;
let edit_button = span.nextElementSibling;
edit_button.addEventListener("click", () => edit_item(prefix, item));
let remove_button = edit_button.nextElementSibling;
remove_button.addEventListener("click",
() => storage.remove(prefix, item));
let export_button = remove_button.nextElementSibling;
export_button.addEventListener("click",
() => export_item(prefix, item));
if (prefix === TYPE_PREFIX.REPO)
export_button.remove();
if (!at_the_end) {
for (let element of ul.ul.children) {
if (element.id < li.id || element.id.startsWith("work_"))
continue;
ul.ul.insertBefore(li, element);
return;
}
}
ul.ul.appendChild(li);
}
const chbx_components_ul = by_id("chbx_components_ul");
const radio_components_ul = by_id("radio_components_ul");
function chbx_li_id(prefix, item)
{
return `cli_${prefix}_${item}`;
}
function radio_li_id(prefix, item)
{
return `rli_${prefix}_${item}`;
}
//TODO: refactor the 2 functions below
function add_chbx_li(prefix, name)
{
if (![TYPE_PREFIX.BAG, TYPE_PREFIX.SCRIPT].includes(prefix))
return;
let li = chbx_component_li_template.cloneNode(true);
li.id = chbx_li_id(prefix, name);
li.setAttribute("data-prefix", prefix);
li.setAttribute("data-name", name);
let chbx = li.firstElementChild;
let span = chbx.nextElementSibling;
span.textContent = nice_name(prefix, name);
chbx_components_ul.appendChild(li);
}
var radio_component_none_li = by_id("radio_component_none_li");
function add_radio_li(prefix, name)
{
if (![TYPE_PREFIX.BAG, TYPE_PREFIX.SCRIPT].includes(prefix))
return;
let li = radio_component_li_template.cloneNode(true);
li.id = radio_li_id(prefix, name);
li.setAttribute("data-prefix", prefix);
li.setAttribute("data-name", name);
let radio = li.firstElementChild;
let span = radio.nextElementSibling;
span.textContent = nice_name(prefix, name);
radio_components_ul.insertBefore(li, radio_component_none_li);
}
/* Used to reset edited repo. */
function reset_work_repo_li(ul, item, _)
{
ul.work_name_input.value = maybe_string(item);
}
/* Used to get repo data for saving */
function work_repo_li_data(ul)
{
return [ul.work_name_input.value, {}];
}
const page_payload_span = by_id("page_payload");
function set_page_components(components)
{
if (components === undefined) {
page_payload_span.setAttribute("data-payload", "no");
page_payload_span.textContent = "(None)";
} else {
page_payload_span.setAttribute("data-payload", "yes");
let [prefix, name] = components;
page_payload_span.setAttribute("data-prefix", prefix);
page_payload_span.setAttribute("data-name", name);
page_payload_span.textContent = nice_name(prefix, name);
}
}
const page_allow_chbx = by_id("page_allow_chbx");
/* Used to reset edited page. */
function reset_work_page_li(ul, item, settings)
{
ul.work_name_input.value = maybe_string(item);
settings = settings || {allow: false, components: undefined};
page_allow_chbx.checked = !!settings.allow;
set_page_components(settings.components);
}
function work_page_li_components()
{
if (page_payload_span.getAttribute("data-payload") === "no")
return undefined;
let prefix = page_payload_span.getAttribute("data-prefix");
let name = page_payload_span.getAttribute("data-name");
return [prefix, name];
}
/* Used to get edited page data for saving. */
function work_page_li_data(ul)
{
let url = ul.work_name_input.value;
let settings = {
components : work_page_li_components(),
allow : !!page_allow_chbx.checked
};
return [url, settings];
}
const empty_bag_component_li = by_id("empty_bag_component_li");
var bag_components_ul = by_id("bag_components_ul");
/* Used to construct and update components list of edited bag. */
function add_bag_components(components)
{
for (let component of components) {
let [prefix, name] = component;
let li = bag_component_li_template.cloneNode(true);
li.setAttribute("data-prefix", prefix);
li.setAttribute("data-name", name);
let span = li.firstElementChild;
span.textContent = nice_name(prefix, name);
let remove_but = span.nextElementSibling;
remove_but.addEventListener("click", () =>
bag_components_ul.removeChild(li));
bag_components_ul.appendChild(li);
}
bag_components_ul.appendChild(empty_bag_component_li);
}
/* Used to reset edited bag. */
function reset_work_bag_li(ul, item, components)
{
components = components || [];
ul.work_name_input.value = maybe_string(item);
let old_components_ul = bag_components_ul;
bag_components_ul = old_components_ul.cloneNode(false);
ul.work_li.insertBefore(bag_components_ul, old_components_ul);
ul.work_li.removeChild(old_components_ul);
console.log("bag components", components);
add_bag_components(components);
}
/* Used to get edited bag data for saving. */
function work_bag_li_data(ul)
{
let components_ul = ul.work_name_input.nextElementSibling;
let component_li = components_ul.firstElementChild;
let components = [];
/* Last list element is empty li with id set. */
while (component_li.id === '') {
components.push([component_li.getAttribute("data-prefix"),
component_li.getAttribute("data-name")]);
component_li = component_li.nextElementSibling;
}
return [ul.work_name_input.value, components];
}
const script_url_input = by_id("script_url_field");
const script_sha256_input = by_id("script_sha256_field");
const script_contents_field = by_id("script_contents_field");
function maybe_string(maybe_defined)
{
return maybe_defined === undefined ? "" : maybe_defined + "";
}
/* Used to reset edited script. */
function reset_work_script_li(ul, name, data)
{
ul.work_name_input.value = maybe_string(name);
if (data === undefined)
data = {};
script_url_input.value = maybe_string(data.url);
script_sha256_input.value = maybe_string(data.hash);
script_contents_field.value = maybe_string(data.text);
}
/* Used to get edited script data for saving. */
function work_script_li_data(ul)
{
return [ul.work_name_input.value, {
url : script_url_input.value,
hash : script_sha256_input.value,
text : script_contents_field.value
}];
}
function cancel_work(prefix)
{
let ul = ul_by_prefix[prefix];
if (ul.state === UL_STATE.IDLE)
return;
if (ul.state === UL_STATE.EDITING_ENTRY) {
add_li(prefix, ul.edited_item);
}
ul.work_li.classList.add("hide");
ul.state = UL_STATE.IDLE;
}
function save_work(prefix)
{
let ul = ul_by_prefix[prefix];
if (ul.state === UL_STATE.IDLE)
return;
let [item, data] = ul.get_work_li_data(ul);
/* Here we fire promises and return without waiting. */
if (ul.state === UL_STATE.EDITING_ENTRY)
storage.replace(prefix, ul.edited_item, item, data);
if (ul.state === UL_STATE.ADDING_ENTRY)
storage.set(prefix, item, data);
cancel_work(prefix);
}
function edit_item(prefix, item)
{
cancel_work(prefix);
let ul = ul_by_prefix[prefix];
let li = by_id(item_li_id(prefix, item));
if (li === null) {
add_new_item(prefix, item);
return;
}
ul.reset_work_li(ul, item, storage.get(prefix, item));
ul.ul.insertBefore(ul.work_li, li);
ul.ul.removeChild(li);
ul.work_li.classList.remove("hide");
ul.state = UL_STATE.EDITING_ENTRY;
ul.edited_item = item;
}
const file_downloader = by_id("file_downloader");
function recursively_export_item(prefix, name, added_items, items_data)
{
let key = prefix + name;
if (added_items.has(key))
return;
let data = storage.get(prefix, name);
if (data === undefined) {
console.log(`${TYPE_NAME[prefix]} '${name}' for export not found`);
return;
}
if (prefix !== TYPE_PREFIX.SCRIPT) {
let components = prefix === TYPE_PREFIX.BAG ?
data : [data.components];
for (let [comp_prefix, comp_name] of components) {
recursively_export_item(comp_prefix, comp_name,
added_items, items_data);
}
}
items_data.push({[key]: data});
added_items.add(key);
}
function export_item(prefix, name)
{
let added_items = new Set();
let items_data = [];
recursively_export_item(prefix, name, added_items, items_data);
let file = new Blob([JSON.stringify(items_data)],
{type: "application/json"});
let url = URL.createObjectURL(file);
file_downloader.setAttribute("href", url);
file_downloader.setAttribute("download", prefix + name + ".json");
file_downloader.click();
file_downloader.removeAttribute("href");
URL.revokeObjectURL(url);
}
function add_new_item(prefix, name)
{
cancel_work(prefix);
let ul = ul_by_prefix[prefix];
ul.reset_work_li(ul);
ul.work_li.classList.remove("hide");
ul.ul.appendChild(ul.work_li);
if (name !== undefined)
ul.work_name_input.value = name;
ul.state = UL_STATE.ADDING_ENTRY;
}
const chbx_components_window = by_id("chbx_components_window");
function bag_components()
{
chbx_components_window.classList.remove("hide");
radio_components_window.classList.add("hide");
for (let li of chbx_components_ul.children) {
let chbx = li.firstElementChild;
chbx.checked = false;
}
}
function commit_bag_components()
{
let selected = [];
for (let li of chbx_components_ul.children) {
let chbx = li.firstElementChild;
if (!chbx.checked)
continue;
selected.push([li.getAttribute("data-prefix"),
li.getAttribute("data-name")]);
}
add_bag_components(selected);
cancel_components();
}
const radio_components_window = by_id("radio_components_window");
var radio_component_none_input = by_id("radio_component_none_input");
function page_components()
{
radio_components_window.classList.remove("hide");
chbx_components_window.classList.add("hide");
radio_component_none_input.checked = true;
let components = work_page_li_components();
if (components === undefined)
return;
let [prefix, item] = components;
let li = by_id(radio_li_id(prefix, item));
if (li === null)
radio_component_none_input.checked = false;
else
li.firstElementChild.checked = true;
}
function commit_page_components()
{
let components = null;
for (let li of radio_components_ul.children) {
let radio = li.firstElementChild;
if (!radio.checked)
continue;
components = [li.getAttribute("data-prefix"),
li.getAttribute("data-name")];
if (radio.id === "radio_component_none_input")
components = undefined;
break;
}
if (components !== null)
set_page_components(components);
cancel_components();
}
function cancel_components()
{
chbx_components_window.classList.add("hide");
radio_components_window.classList.add("hide");
}
const UL_STATE = {
EDITING_ENTRY : 0,
ADDING_ENTRY : 1,
IDLE : 2
};
const ul_by_prefix = {
[TYPE_PREFIX.REPO] : {
ul : by_id("repos_ul"),
work_li : by_id("work_repo_li"),
work_name_input : by_id("repo_url_field"),
reset_work_li : reset_work_repo_li,
get_work_li_data : work_repo_li_data,
state : UL_STATE.IDLE,
edited_item : undefined,
},
[TYPE_PREFIX.PAGE] : {
ul : by_id("pages_ul"),
work_li : by_id("work_page_li"),
work_name_input : by_id("page_url_field"),
reset_work_li : reset_work_page_li,
get_work_li_data : work_page_li_data,
select_components : page_components,
commit_components : commit_page_components,
state : UL_STATE.IDLE,
edited_item : undefined,
},
[TYPE_PREFIX.BAG] : {
ul : by_id("bags_ul"),
work_li : by_id("work_bag_li"),
work_name_input : by_id("bag_name_field"),
reset_work_li : reset_work_bag_li,
get_work_li_data : work_bag_li_data,
select_components : bag_components,
commit_components : commit_bag_components,
state : UL_STATE.IDLE,
edited_item : undefined,
},
[TYPE_PREFIX.SCRIPT] : {
ul : by_id("scripts_ul"),
work_li : by_id("work_script_li"),
work_name_input : by_id("script_name_field"),
reset_work_li : reset_work_script_li,
get_work_li_data : work_script_li_data,
state : UL_STATE.IDLE,
edited_item : undefined,
}
}
const import_window = by_id("import_window");
const import_loading_radio = by_id("import_loading_radio");
const import_failed_radio = by_id("import_failed_radio");
const import_selection_radio = by_id("import_selection_radio");
const bad_file_errormsg = by_id("bad_file_errormsg");
/*
* Newer browsers could utilise `text' method of File objects.
* Older ones require FileReader.
*/
function _read_file(file, resolve, reject)
{
let reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsText(file);
}
function read_file(file)
{
return new Promise((resolve, reject) =>
_read_file(file, resolve, reject));
}
const url_regex = /^[a-z0-9]+:\/\/[^/]+\.[^/]{2}(\/[^?#]*)?$/;
const sha256_regex = /^[0-9a-f]{64}$/;
const component_schema = [
new RegExp(`^[${TYPE_PREFIX.SCRIPT}${TYPE_PREFIX.BAG}]$`),
/.+/
];
const settings_schema = [
[{}, "matchentry", "minentries", 1,
new RegExp(`^${TYPE_PREFIX.SCRIPT}`), {
/* script data */
"url": ["optional", url_regex],
"sha256": ["optional", sha256_regex],
"text": ["optional", "string"]
},
new RegExp(`^${TYPE_PREFIX.BAG}`), [
"optional",
[component_schema, "repeat"],
"default", undefined
],
new RegExp(`^${TYPE_PREFIX.PAGE}`), {
/* page data */
"components": ["optional", component_schema]
}], "repeat"
]
async function import_from_file(event)
{
let files = event.target.files;
if (files.length < 1)
return;
import_window.classList.remove("hide");
import_loading_radio.checked = true;
let result = undefined;
try {
result = parse_json_with_schema(settings_schema,
await read_file(files[0]));
} catch(e) {
bad_file_errormsg.textContent = "" + e;
import_failed_radio.checked = true;
return;
}
populate_import_list(result);
import_selection_radio.checked = true;
}
function import_li_id(prefix, item)
{
return `ili_${prefix}_${item}`;
}
let import_ul = by_id("import_ul");
let import_chbxs_colliding = undefined;
let settings_import_map = undefined;
function populate_import_list(settings)
{
let old_children = import_ul.children;
while (old_children[0] !== undefined)
import_ul.removeChild(old_children[0]);
import_chbxs_colliding = [];
settings_import_map = new Map();
for (let setting of settings) {
let [key, value] = Object.entries(setting)[0];
let prefix = key[0];
let name = key.substring(1);
add_import_li(prefix, name);
settings_import_map.set(key, value);
}
}
function add_import_li(prefix, name)
{
let li = import_li_template.cloneNode(true);
let name_span = li.firstElementChild;
let chbx = name_span.nextElementSibling;
let warning_span = chbx.nextElementSibling;
li.setAttribute("data-prefix", prefix);
li.setAttribute("data-name", name);
li.id = import_li_id(prefix, name);
name_span.textContent = nice_name(prefix, name);
if (storage.get(prefix, name) !== undefined) {
import_chbxs_colliding.push(chbx);
warning_span.textContent = "(will overwrite existing setting!)";
}
import_ul.appendChild(li);
}
function check_all_imports()
{
for (let li of import_ul.children)
li.firstElementChild.nextElementSibling.checked = true;
}
function uncheck_all_imports()
{
for (let li of import_ul.children)
li.firstElementChild.nextElementSibling.checked = false;
}
function uncheck_colliding_imports()
{
for (let chbx of import_chbxs_colliding)
chbx.checked = false;
}
const file_opener_form = by_id("file_opener_form");
function hide_import_window()
{
import_window.classList.add("hide");
/* Let GC free some memory */
import_chbxs_colliding = undefined;
settings_import_map = undefined;
/*
* Reset file . Without this, a second attempt to import the same
* file would result in "change" event not happening on element.
*/
file_opener_form.reset();
}
function commit_import()
{
for (let li of import_ul.children) {
let chbx = li.firstElementChild.nextElementSibling;
if (!chbx.checked)
continue;
let prefix = li.getAttribute("data-prefix");
let name = li.getAttribute("data-name");
let key = prefix + name;
let value = settings_import_map.get(key);
storage.set(prefix, name, value);
}
hide_import_window();
}
function initialize_import_facility()
{
let import_but = by_id("import_but");
let file_opener = by_id("file_opener");
let import_failok_but = by_id("import_failok_but");
let check_all_import_but = by_id("check_all_import_but");
let uncheck_all_import_but = by_id("uncheck_all_import_but");
let uncheck_existing_import_but = by_id("uncheck_existing_import_but");
let commit_import_but = by_id("commit_import_but");
let cancel_import_but = by_id("cancel_import_but");
import_but.addEventListener("click", () => file_opener.click());
file_opener.addEventListener("change", import_from_file);
import_failok_but.addEventListener("click", hide_import_window);
check_all_import_but.addEventListener("click", check_all_imports);
uncheck_all_import_but.addEventListener("click", uncheck_all_imports);
uncheck_colliding_import_but
.addEventListener("click", uncheck_colliding_imports);
commit_import_but.addEventListener("click", commit_import);
cancel_import_but.addEventListener("click", hide_import_window);
}
/*
* If url has a target appended, e.g.
* chrome-extension://hnhmbnpohhlmhehionjgongbnfdnabdl/html/options.html#smyhax
* that target will be split into prefix and item name (e.g. "s" and "myhax")
* and editing of that respective item will be started.
*
* We don't need to worry about the state of the page (e.g. some editing being
* in progress) in jump_to_item() - this function is called at the beginning,
* together with callbacks being assigned to buttons, so it is safe to assume
* lists are initialized with items and page is in its virgin state with regard
* to everything else.
*/
function jump_to_item(url_with_item)
{
const [dummy1, base_url, dummy2, target] =
/^([^#]*)(#(.*))?$/i.exec(url_with_item);
if (target === undefined)
return;
const prefix = target.substring(0, 1);
if (!list_prefixes.includes(prefix)) {
history.replaceState(null, "", base_url);
return;
}
by_id(`show_${TYPE_NAME[prefix]}s`).checked = true;
edit_item(prefix, decodeURIComponent(target.substring(1)));
}
async function main()
{
storage = await get_remote_storage();
for (let prefix of list_prefixes) {
for (let item of storage.get_all_names(prefix).sort()) {
add_li(prefix, item, true);
add_chbx_li(prefix, item);
add_radio_li(prefix, item);
}
let name = TYPE_NAME[prefix];
let add_but = by_id(`add_${name}_but`);
let discard_but = by_id(`discard_${name}_but`);
let save_but = by_id(`save_${name}_but`);
add_but.addEventListener("click", () => add_new_item(prefix));
discard_but.addEventListener("click", () => cancel_work(prefix));
save_but.addEventListener("click", () => save_work(prefix));
if ([TYPE_PREFIX.REPO, TYPE_PREFIX.SCRIPT].includes(prefix))
continue;
let ul = ul_by_prefix[prefix];
let commit_components_but = by_id(`commit_${name}_components_but`);
let cancel_components_but = by_id(`cancel_${name}_components_but`);
let select_components_but = by_id(`select_${name}_components_but`);
commit_components_but
.addEventListener("click", ul.commit_components);
select_components_but
.addEventListener("click", ul.select_components);
cancel_components_but.addEventListener("click", cancel_components);
}
jump_to_item(document.URL);
initialize_import_facility();
storage.add_change_listener(handle_change);
}
function handle_change(change)
{
if (change.old_val === undefined) {
add_li(change.prefix, change.item);
add_chbx_li(change.prefix, change.item);
add_radio_li(change.prefix, change.item);
return;
}
if (change.new_val !== undefined)
return;
let ul = ul_by_prefix[change.prefix];
if (ul.state === UL_STATE.EDITING_ENTRY &&
ul.edited_item === change.item) {
ul.state = UL_STATE.ADDING_ENTRY;
return;
}
let uls_creators = [[ul.ul, item_li_id]];
if ([TYPE_PREFIX.BAG, TYPE_PREFIX.SCRIPT].includes(change.prefix)) {
uls_creators.push([chbx_components_ul, chbx_li_id]);
uls_creators.push([radio_components_ul, radio_li_id]);
}
for (let [components_ul, id_creator] of uls_creators) {
let li = by_id(id_creator(change.prefix, change.item));
components_ul.removeChild(li);
}
}
main();