From 4c6a2323d90e9321ec2b78e226167b3013ea69ab Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Sat, 29 Jan 2022 00:03:51 +0100 Subject: make Haketilo buildable again (for Mozilla) How cool it is to throw away 5755 lines of code... --- html/options_main.js | 781 --------------------------------------------------- 1 file changed, 781 deletions(-) delete mode 100644 html/options_main.js (limited to 'html/options_main.js') diff --git a/html/options_main.js b/html/options_main.js deleted file mode 100644 index 4b828f0..0000000 --- a/html/options_main.js +++ /dev/null @@ -1,781 +0,0 @@ -/** - * This file is part of Haketilo. - * - * Function: Settings page logic. - * - * Copyright (C) 2021 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 . - * - * 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. - */ - -#FROM common/storage_client.js IMPORT get_remote_storage -#FROM common/stored_types.js IMPORT TYPE_PREFIX, TYPE_NAME, \ - list_prefixes -#FROM common/misc.js IMPORT nice_name, matchers -#FROM common/sanitize_JSON.js IMPORT parse_json_with_schema -#FROM html/DOM_helpers.js IMPORT get_template, by_id -#FROM html/import_frame.js IMPORT get_import_frame - -var storage; - -const item_li_template = get_template("item_li"); -const bag_component_li_template = get_template("bag_component_li"); -const chbx_component_li_template = get_template("chbx_component_li"); -const radio_component_li_template = get_template("radio_component_li"); -/* 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"); - -function list_set_scrollbar(list_elem) -{ - const op = ((list_elem.children.length === 1 && - list_elem.children[0].classList.contains("hide")) || - list_elem.children.length < 1) ? "remove" : "add"; - while (!list_elem.classList.contains("table_wrapper")) - list_elem = list_elem.parentElement; - list_elem.classList[op]("always_scrollbar"); -} - -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); - break; - } - } - if (!li.parentElement) { - if (ul.work_li !== ul.ul.lastElementChild) - ul.ul.appendChild(li); - else - ul.work_li.before(li); - } - - list_set_scrollbar(ul.ul); -} - -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.firstElementChild; - let span = chbx.nextElementSibling; - - span.textContent = nice_name(prefix, name); - - chbx_components_ul.appendChild(li); - list_set_scrollbar(chbx_components_ul); -} - -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.firstElementChild; - let span = radio.nextElementSibling; - - span.textContent = nice_name(prefix, name); - - radio_component_none_li.before(li); - list_set_scrollbar(radio_components_ul); -} - -/* 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 allow_native_scripts_container = by_id("allow_native_scripts_container"); -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)"; - allow_native_scripts_container.classList.remove("form_disabled"); - } 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); - allow_native_scripts_container.classList.add("form_disabled"); - } -} - -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"); - -function remove_bag_component_entry(entry) -{ - const list = entry.parentElement; - entry.remove(); - list_set_scrollbar(list); -} - -/* 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", - () => remove_bag_component_entry(li)); - bag_components_ul.appendChild(li); - } - - bag_components_ul.appendChild(empty_bag_component_li); - list_set_scrollbar(bag_components_ul); -} - -/* 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); - - old_components_ul.replaceWith(bag_components_ul); - - add_bag_components(components); -} - -/* Used to get edited bag data for saving. */ -function work_bag_li_data(ul) -{ - let component_li = bag_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.ul.append(ul.work_li); - list_set_scrollbar(ul.ul); - 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"); - list_set_scrollbar(ul.ul); - - 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); - list_set_scrollbar(ul.ul); - - 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.firstElementChild; - chbx.checked = false; - } -} - -function commit_bag_components() -{ - let selected = []; - - for (let li of chbx_components_ul.children) { - let chbx = li.firstElementChild.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.firstElementChild.checked = true; -} - -function commit_page_components() -{ - let components = null; - - for (let li of radio_components_ul.children) { - let radio = li.firstElementChild.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, - } -} - -/* - * 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 empty_regex = /^$/; - -const settings_schema = [ - [{}, "matchentry", "minentries", 1, - new RegExp(`^${TYPE_PREFIX.SCRIPT}`), { - /* script data */ - "url": ["optional", url_regex, "or", empty_regex], - "sha256": ["optional", matchers.sha256, "or", empty_regex], - "text": ["optional", "string"] - }, - new RegExp(`^${TYPE_PREFIX.BAG}`), [ - "optional", - [matchers.component, "repeat"], - "default", undefined - ], - new RegExp(`^${TYPE_PREFIX.PAGE}`), { - /* page data */ - "components": ["optional", matchers.component] - }], "repeat" -]; - -const import_window = by_id("import_window"); -let import_frame; - -async function import_from_file(event) -{ - let files = event.target.files; - if (files.length < 1) - return; - - import_window.classList.remove("hide"); - import_frame.show_loading(); - - try { - const file = await read_file(files[0]); - var result = parse_json_with_schema(settings_schema, file); - } catch(e) { - import_frame.show_error("Bad file :(", "" + e); - return; - } - - import_frame.show_selection(result); -} - -const file_opener_form = by_id("file_opener_form"); - -function hide_import_window() -{ - import_window.classList.add("hide"); - - /* - * 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(); -} - -async function initialize_import_facility() -{ - let import_but = by_id("import_but"); - let file_opener = by_id("file_opener"); - - import_but.addEventListener("click", () => file_opener.click()); - file_opener.addEventListener("change", import_from_file); - - import_frame = await get_import_frame(); - import_frame.onclose = hide_import_window; - import_frame.style_table("has_bottom_line", "always_scrollbar", - "has_upper_line", "tight_table"); -} - -/* - * 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); - - storage.add_change_listener(handle_change); - - await initialize_import_facility(); -} - -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); - list_set_scrollbar(components_ul); - } -} - -main(); -- cgit v1.2.3