diff options
author | Wojtek Kosior <koszko@koszko.org> | 2022-01-29 00:03:51 +0100 |
---|---|---|
committer | Wojtek Kosior <koszko@koszko.org> | 2022-01-29 00:04:44 +0100 |
commit | 4c6a2323d90e9321ec2b78e226167b3013ea69ab (patch) | |
tree | 682ab3abd53a68d28d04f0470766699dcadd7294 /html | |
parent | ea9df6c7688613783ca114f0f11c6f6baf30036b (diff) | |
download | browser-extension-4c6a2323d90e9321ec2b78e226167b3013ea69ab.tar.gz browser-extension-4c6a2323d90e9321ec2b78e226167b3013ea69ab.zip |
make Haketilo buildable again (for Mozilla)
How cool it is to throw away 5755 lines of code...
Diffstat (limited to 'html')
-rw-r--r-- | html/back_button.css | 71 | ||||
-rw-r--r-- | html/display_panel.html | 370 | ||||
-rw-r--r-- | html/display_panel.js | 586 | ||||
-rw-r--r-- | html/import_frame.html | 79 | ||||
-rw-r--r-- | html/import_frame.js | 185 | ||||
-rw-r--r-- | html/mozilla_scrollbar_fix.css | 67 | ||||
-rw-r--r-- | html/options.html | 416 | ||||
-rw-r--r-- | html/options_main.js | 781 | ||||
-rw-r--r-- | html/popup.html | 2 | ||||
-rw-r--r-- | html/settings.html | 1 | ||||
-rw-r--r-- | html/table.css | 74 |
11 files changed, 1 insertions, 2631 deletions
diff --git a/html/back_button.css b/html/back_button.css deleted file mode 100644 index 38740d7..0000000 --- a/html/back_button.css +++ /dev/null @@ -1,71 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 - * - * Style for a "back" button with a CSS arrow image - * - * This file is part of Haketilo. - * - * Copyright (C) 2021 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. - */ - -.back_button { - display: block; - width: auto; - height: auto; - background-color: white; - border: solid #454 0.4em; - border-left: none; - border-radius: 0 1.5em 1.5em 0; - cursor: pointer; -} - -.back_button:hover { - box-shadow: 0 6px 8px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19); -} - -.back_button>div, .back_arrow { - width: 2em; - height: 0.5em; - background-color: #4CAF50; - border-radius: 0.3em; - margin: 1.15em 0.4em; -} - -.back_button>div::after, .back_arrow::after, -.back_button>div::before, .back_arrow::before { - content: ""; - display: block; - position: relative; - background-color: inherit; - width: 1.3em; - height: 0.5em; - transform: rotate(45deg); - border-radius: 0.3em; - top: 0.3em; - right: 0.2em; - margin: 0 -1.3em -0.5em 0; -} - -.back_button>div::before, .back_arrow::before { - transform: rotate(-45deg); - top: -0.3em; -} diff --git a/html/display_panel.html b/html/display_panel.html deleted file mode 100644 index 1468368..0000000 --- a/html/display_panel.html +++ /dev/null @@ -1,370 +0,0 @@ -<!doctype html> -<!-- - SPDX-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 - - Extension's popup page - - This file is part of Haketilo. - - Copyright (C) 2021 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. - --> -<html> - <head> - <meta charset="utf-8"/> - <title>Haketilo - page settings</title> -#LOADCSS html/reset.css -#LOADCSS html/base.css -#LOADCSS html/back_button.css -#LOADCSS html/table.css -#IF MOZILLA -#LOADCSS html/mozilla_scrollbar_fix.css -#ENDIF - <style> - body { - width: max-content; - width: -moz-fit-content; - } - - .top>h2 { - padding-left: calc(0.8*3.2em - 8px); - } - - .top { - line-height: calc(0.8*3.6em - 16px); - } - - #main_view>.top>h2 { - padding-left: 0; - max-width: 550px - } - - .unroll_chbx:not(:checked)+div>:not(:first-child) { - display: none; - } - - .unroll_triangle { - height: 1em; - width: 1em; - display: inline-block; - } - - .unroll_triangle::after { - content: ""; - width: 0.6em; - height: 0.6em; - background: linear-gradient(-45deg, currentColor 50%, transparent 50%); - display: block; - position: relative; - transform: rotate(-45deg); - top: 0.3em; - } - - .unroll_chbx:checked+div>:first-child .unroll_triangle::after { - transform: rotate(45deg); - left: 0.2em; - top: 0.2em; - } - - .unroll_chbx:checked+div>:first-child .unroll_block { - display: block; - } - - .unroll_chbx:checked+div>:first-child { - line-height: 1.4em; - } - - .l2_ul { - border-left: solid #454 5px; - } - - .l1_li { - margin-top: 0.3em; - margin-bottom: 0.3em; - } - - .l1_li>div { - padding: 0.3em 0.3em 0.3em 0; - } - - .l2_li { - padding: 0.3em; - } - - #container_for_injected>*:nth-child(odd), - .l2_li:nth-child(odd) { - background-color: #e5e5e5; - } - - #container_for_injected>#none_injected:not(:last-child) { - display: none; - } - - #page_url_heading>span { - display: inline-block; - } - - .back_button { - position: fixed; - z-index: 1; - top: 0; - left: 0; - /* The following scales the entire button. */ - font-size: 80%; - } - - #show_main_view_radio:checked~.back_button { - margin-left: -3.2em; - } - - #show_main_view_radio:not(:checked)~.back_button { - transition: all 0.2s ease-out; - } - - pre { - font-family: monospace; - background-color: white; - padding: 1px 5px; - } - - .matched_pattern { - font-weight: bold; - } - - tr.matched_pattern~tr { - color: #777; - font-size: 90%; - } - - .padding_inline { - padding-left: 5px; - padding-right: 5px; - } - - .padding_top { - padding-top: 5px; - } - - .header { - padding-bottom: 0.3em; - margin-bottom: 0.5em; - text-align: center; - } - - .middle { - margin-top: 0.5em; - margin-bottom: 0.5em; - } - - .footer { - padding-top: 0.3em; - margin-top: 0.5em; - text-align: center; - } - - .active_setting_table { - margin-bottom: 0.5em; - } - - .active_setting_table td { - padding: 5px; - vertical-align: middle; - } - </style> - </head> - <body> - <template> - <tr id="pattern_entry" class="nowrap" data-template="entry"> - <td data-template="name"></td> - <td> - <div class="button" data-template="button">Add setting</div> - </td> - </tr> - - <li id="query_match_li" class="l2_li" data-template="li"> - <div> - <span>pattern:</span> - <span class="bold" data-template="pattern"></span> - <label class="button slimbutton" for="show_install_view_radio" data-template="btn"> - Install - </label> - </div> - <div id="unrollable_component" data-template="unroll_container"> - <input type="checkbox" class="unroll_chbx" data-template="chbx"></input> - <div> - <span>payload: - <label class="bold unroll_block" data-template="lbl"> - <div data-template="triangle" class="unroll_triangle"></div> - <span data-template="payload"></span> - </label> - </span> - <div data-template="unroll"></div> - </div> - </div> - </li> - - <div id="injected_script" data-template="div"> - <input type="checkbox" class="unroll_chbx" data-template="chbx"></input> - <div> - <label data-template="lbl"> - <h3><div class="unroll_triangle"></div> script</h3> - </label> - <pre class="has_bottom_thin_line has_upper_thin_line" data-template="script_contents"></pre> - </div> - </div> - - <div id="multi_repos_query_result" data-template="div"> - Results for <span class="bold" data-template="url_span"></span> - <ul class="l1_ul" data-template="ul"></ul> - </div> - - <li id="single_repo_query_result" class="l1_li" data-template="li"> - <div> - From <span class="bold" data-template="repo_url"></span> - </div> - </li> - - <ul id="result_patterns_list" class="l2_ul" data-template="ul"> - </ul> - </template> - - <input id="show_install_view_radio" type="radio" class="show_next" name="current_view"></input> - <div id="install_view"> - <div class="top has_bottom_line"><h2> Site modifiers install </h2></div> - <div class="padding_inline"> -#INCLUDE html/import_frame.html - </div> - </div> - - <input id="show_injected_view_radio" type="radio" class="show_next" name="current_view"></input> - <div id="injected_view"> - <div class="top has_bottom_line"><h2>Injected scripts</h2></div> - <div id="container_for_injected"> - <span id="none_injected">None</span> - </div> - </div> - - <input id="show_patterns_view_radio" type="radio" class="show_next" name="current_view"></input> - <div> - <div class="top has_bottom_line"><h2>Possible patterns for this page</h2></div> - <div class="padding_inline"> - <aside> - Patterns higher are more specific and override the ones below. - </aside> - </div> - <div class="table_wrapper firefox_scrollbars_hacky_fix"> - <div> - <table> - <tbody id="possible_patterns"> - </tbody> - </table> - </div> - </div> - <div class="padding_inline padding_top has_upper_thin_line firefox_scrollbars_hacky_fix has_inline_content"> - <span class="nowrap"> -#INCLUDE html/default_blocking_policy.html - </span> - </div> - </div> - - <input id="show_queried_view_radio" type="radio" class="show_next" name="current_view"></input> - <div> - <div class="top has_bottom_line"><h2>Queried from repositories</h2></div> - <div id="container_for_repo_responses" class="padding_inline"> - </div> - </div> - - <input id="show_main_view_radio" type="radio" class="show_next" name="current_view" checked></input> - <div id="main_view"> - <div class="top has_bottom_line"><h2 id="page_url_heading"></h2></div> - <h3 id="privileged_notice" class="middle hide">Privileged page</h3> - - <div id="page_state" class="hide"> - <div class="header padding_inline has_bottom_thin_line"> - <label for="show_patterns_view_radio" class="button"> - Edit settings for this page - </label> - </div> - <div class="middle padding_inline"> - <input id="connected_chbx" type="checkbox" class="show_hide_next2"></input> - <div> - <table class="active_setting_table"> - <tbody> - <tr class="nowrap"> - <td>Matched pattern:</td> - <td id="pattern" class="bold">...</td> - <td> - <button id="view_pattern" class="hide"> - View in settings - </button> - </td> - </tr> - <tr class="nowrap"> - <td>Scripts blocked:</td> - <td id="blocked" class="bold">...</td> - <td></td> - </tr> - <tr class="nowrap"> - <td>Injected payload:</td> - <td id="payload" class="bold">...</td> - <td id="payload_buttons" class="hide"> - <button id="view_payload"> View in settings </button> - <br/> - <label id="view_injected" class="button" for="show_injected_view_radio"> - View injected scripts - </label> - </td> - </tr> - <tr> - <td id="content_type" colspan="3" class="hide"> - This is a non-HTML page. Chosen payload will not be injected. - </td> - </tr> - </tbody> - </table> - <label id="query_pattern" for="show_queried_view_radio" class="button"> - Install scripts for this page - </label> - </div> - <div> - <h3> - Connecting to content script..<span id="loading_point">.</span> - </h3> - <aside id="reload_notice"> - Try reloading the page. - </aside> - </div> - </div> - </div> - - <div class="footer padding_inline has_upper_thin_line"> - <button id="settings_but" type="button"> - Open Haketilo settings - </button> - </div> - </div> - - <div class="has_upper_line"></div> - - <label for="show_main_view_radio" class="back_button"><div></div></label> -#LOADJS html/display_panel.js - </body> -</html> diff --git a/html/display_panel.js b/html/display_panel.js deleted file mode 100644 index 1cd77e6..0000000 --- a/html/display_panel.js +++ /dev/null @@ -1,586 +0,0 @@ -/** - * This file is part of Haketilo. - * - * Function: Popup 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 <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 common/connection_types.js AS CONNECTION_TYPE - -#FROM common/browser.js IMPORT browser -/* - * Using remote storage here seems inefficient, we only resort to that - * temporarily, before all storage access gets reworked. - */ -#FROM common/storage_client.js IMPORT get_remote_storage -#FROM html/import_frame.js IMPORT get_import_frame -#FROM common/settings_query.js IMPORT query_all -#FROM common/misc.js IMPORT is_privileged_url, nice_name, \ - open_in_settings -#FROM common/stored_types.js IMPORT TYPE_PREFIX -#FROM common/patterns.js IMPORT each_url_pattern -#FROM html/DOM_helpers.js IMPORT by_id, clone_template - -let storage; -let tab_url; - -#IF MOZILLA -/* Force popup <html>'s reflow on stupid Firefox. */ -const reflow_forcer = - () => document.documentElement.style.width = "-moz-fit-content"; -for (const radio of document.querySelectorAll('[name="current_view"]')) - radio.addEventListener("change", reflow_forcer); -#ENDIF - -const show_queried_view_radio = by_id("show_queried_view_radio"); - -const tab_query = {currentWindow: true, active: true}; - -async function get_current_tab() -{ -#IF CHROMIUM - const callback = (cb) => browser.tabs.query(tab_query, tab => cb(tab)); - const promise = new Promise(callback); -#ELIF MOZILLA - const promise = browser.tabs.query(tab_query); -#ENDIF - - try { - return (await promise)[0]; - } catch(e) { - console.log(e); - } -} - -const page_url_heading = by_id("page_url_heading"); -const privileged_notice = by_id("privileged_notice"); -const page_state = by_id("page_state"); - -/* Helper functions to convert string into a list of one-letter <span>'s. */ -function char_to_span(char, doc) -{ - const span = document.createElement("span"); - span.textContent = char; - return span; -} - -function to_spans(string, doc=document) -{ - return string.split("").map(c => char_to_span(c, doc)); -} - -async function show_page_activity_info() -{ - const tab = await get_current_tab(); - - if (tab === undefined) { - page_url_heading.textContent = "unknown page"; - return; - } - - tab_url = /^([^?#]*)/.exec(tab.url)[1]; - to_spans(tab_url).forEach(s => page_url_heading.append(s)); - if (is_privileged_url(tab_url)) { - privileged_notice.classList.remove("hide"); - return; - } - - populate_possible_patterns_list(tab_url); - page_state.classList.remove("hide"); - - try_to_connect(tab.id); -} - -const possible_patterns_list = by_id("possible_patterns"); -const known_patterns = new Map(); - -function add_pattern_to_list(pattern) -{ - const template = clone_template("pattern_entry"); - template.name.textContent = pattern; - - const settings_opener = () => open_in_settings(TYPE_PREFIX.PAGE, pattern); - template.button.addEventListener("click", settings_opener); - - known_patterns.set(pattern, template); - possible_patterns_list.append(template.entry); - - return template; -} - -function style_possible_pattern_entry(pattern, exists_in_settings) -{ - const [text, class_action] = exists_in_settings ? - ["Edit", "add"] : ["Add", "remove"]; - const entry_object = known_patterns.get(pattern); - - if (entry_object) { - entry_object.button.textContent = `${text} setting`; - entry_object.entry.classList[class_action]("matched_pattern"); - } -} - -function handle_page_change(change) -{ - style_possible_pattern_entry(change.item, change.new_val !== undefined); -} - -function populate_possible_patterns_list(url) -{ - for (const pattern of each_url_pattern(url)) - add_pattern_to_list(pattern); - - for (const [pattern, settings] of query_all(storage, url)) - style_possible_pattern_entry(pattern, true); - - storage.add_change_listener(handle_page_change, [TYPE_PREFIX.PAGE]); -} - -const connected_chbx = by_id("connected_chbx"); -const query_pattern_but = by_id("query_pattern"); - -var content_script_port; - -function try_to_connect(tab_id) -{ - /* This won't connect to iframes. We'll add support for them later */ - const connect_info = {name: CONNECTION_TYPE.ACTIVITY_INFO, frameId: 0}; - content_script_port = browser.tabs.connect(tab_id, connect_info); - - const disconnect_cb = () => handle_disconnect(tab_id, start_querying_repos); - content_script_port.onDisconnect.addListener(disconnect_cb); - content_script_port.onMessage.addListener(handle_activity_report); - - query_pattern_but.addEventListener("click", start_querying_repos); - -#IF MOZILLA - setTimeout(() => monitor_connecting(tab_id), 1000); -#ENDIF -} - -function start_querying_repos() -{ - query_pattern_but.removeEventListener("click", start_querying_repos); - const repo_urls = storage.get_all_names(TYPE_PREFIX.REPO); - if (content_script_port) - content_script_port.postMessage([TYPE_PREFIX.URL, tab_url, repo_urls]); -} - -const loading_point = by_id("loading_point"); -const reload_notice = by_id("reload_notice"); - -function handle_disconnect(tab_id, button_cb) -{ - query_pattern_but.removeEventListener("click", button_cb); - content_script_port = null; - -#IF CHROMIUM - if (!browser.runtime.lastError) - return; -#ENDIF - - /* return if error was not during connection initialization */ - if (connected_chbx.checked) - return; - - loading_point.classList.toggle("camouflage"); - reload_notice.classList.remove("hide"); - - setTimeout(() => try_to_connect(tab_id), 1000); -} - -function monitor_connecting(tab_id) -{ - if (connected_chbx.checked) - return; - - if (content_script_port) - content_script_port.disconnect(); - else - return; - - loading_point.classList.toggle("camouflage"); - reload_notice.classList.remove("hide"); - try_to_connect(tab_id); -} - -const pattern_span = by_id("pattern"); -const view_pattern_but = by_id("view_pattern"); -const blocked_span = by_id("blocked"); -const payload_span = by_id("payload"); -const payload_buttons_div = by_id("payload_buttons"); -const view_payload_but = by_id("view_payload"); -const view_injected_but = by_id("view_injected"); -const container_for_injected = by_id("container_for_injected"); -const content_type_cell = by_id("content_type"); - -const queried_items = new Map(); - -let max_injected_script_id = 0; - -function handle_activity_report(message) -{ - connected_chbx.checked = true; - - const [type, data] = message; - - if (type === "settings") { - const settings = data; - - blocked_span.textContent = settings.allow ? "no" : "yes"; - - if (settings.pattern) { - pattern_span.textContent = settings.pattern; - const settings_opener = - () => open_in_settings(TYPE_PREFIX.PAGE, settings.pattern); - view_pattern_but.classList.remove("hide"); - view_pattern_but.addEventListener("click", settings_opener); - } else { - pattern_span.textContent = "none"; - blocked_span.textContent = blocked_span.textContent + " (default)"; - } - - if (settings.payload) { - payload_span.textContent = nice_name(...settings.payload); - payload_buttons_div.classList.remove("hide"); - const settings_opener = () => open_in_settings(...settings.payload); - view_payload_but.addEventListener("click", settings_opener); - } else { - payload_span.textContent = "none"; - } - } - if (type === "script") { - const template = clone_template("injected_script"); - const chbx_id = `injected_script_${max_injected_script_id++}`; - template.chbx.id = chbx_id; - template.lbl.setAttribute("for", chbx_id); - template.script_contents.textContent = data; - container_for_injected.appendChild(template.div); - } - if (type === "is_html") { - if (!data) - content_type_cell.classList.remove("hide"); - } - if (type === "repo_query_action") { - const key = data.prefix + data.item; - const results = queried_items.get(key) || {}; - Object.assign(results, data.results); - queried_items.set(key, results); - - const action = data.prefix === TYPE_PREFIX.URL ? - show_query_result : record_fetched_install_dep; - - for (const [repo_url, result] of Object.entries(data.results)) - action(data.prefix, data.item, repo_url, result); - } -} - -const container_for_repo_responses = by_id("container_for_repo_responses"); - -const results_lists = new Map(); - -function create_results_list(url) -{ - const cloned_template = clone_template("multi_repos_query_result"); - cloned_template.url_span.textContent = url; - container_for_repo_responses.appendChild(cloned_template.div); - - cloned_template.by_repo = new Map(); - results_lists.set(url, cloned_template); - - return cloned_template; -} - -function create_result_item(list_object, repo_url, result) -{ - const cloned_template = clone_template("single_repo_query_result"); - cloned_template.repo_url.textContent = repo_url; - cloned_template.appended = null; - - list_object.ul.appendChild(cloned_template.li); - list_object.by_repo.set(repo_url, cloned_template); - - return cloned_template; -} - -function set_appended(result_item, element) -{ - if (result_item.appended) - result_item.appended.remove(); - result_item.appended = element; - result_item.li.appendChild(element); -} - -function show_message(result_item, text) -{ - const div = document.createElement("div"); - div.textContent = text; - set_appended(result_item, div); -} - -function showcb(text) -{ - return item => show_message(item, text); -} - -function unroll_chbx_first_checked(entry_object) -{ - if (!entry_object.chbx.checked) - return; - - entry_object.chbx.removeEventListener("change", entry_object.unroll_cb); - delete entry_object.unroll_cb; - - entry_object.unroll.innerHTML = "preview not implemented...<br />(consider contributing)"; -} - -let import_frame; -let install_target = null; - -function install_abort(error_state) -{ - import_frame.show_error(`Error: ${error_state}`); - install_target = null; -} - -/* - * Translate objects from the format in which they are sent by Hydrilla to the - * format in which they are stored in settings. - */ - -function translate_script(script_object, repo_url) -{ - return { - [TYPE_PREFIX.SCRIPT + script_object.name]: { - hash: script_object.sha256, - url: `${repo_url}/content/${script_object.location}` - } - }; -} - -function translate_bag(bag_object) -{ - return { - [TYPE_PREFIX.BAG + bag_object.name]: bag_object.components - }; -} - -const format_translators = { - [TYPE_PREFIX.BAG]: translate_bag, - [TYPE_PREFIX.SCRIPT]: translate_script -}; - -function install_check_ready() -{ - if (install_target.to_fetch.size > 0) - return; - - const page_key = [TYPE_PREFIX.PAGE + install_target.pattern]; - const to_install = [{[page_key]: {components: install_target.payload}}]; - - for (const key of install_target.fetched) { - const old_object = - queried_items.get(key)[install_target.repo_url].response; - const new_object = - format_translators[key[0]](old_object, install_target.repo_url); - to_install.push(new_object); - } - - import_frame.show_selection(to_install); -} - -const possible_errors = ["connection_error", "parse_error"]; - -function fetch_install_deps(components) -{ - const needed = [...components]; - const processed = new Set(); - - while (needed.length > 0) { - const [prefix, item] = needed.pop(); - const key = prefix + item; - processed.add(key); - const results = queried_items.get(key); - let relevant_result = null; - - if (results) - relevant_result = results[install_target.repo_url]; - - if (!relevant_result) { - content_script_port.postMessage([prefix, item, - [install_target.repo_url]]); - install_target.to_fetch.add(key); - continue; - } - - if (possible_errors.includes(relevant_result.state)) { - install_abort(relevant_result.state); - return false; - } - - install_target.fetched.add(key); - - if (prefix !== TYPE_PREFIX.BAG) - continue; - - for (const dependency of relevant_result.response.components) { - if (processed.has(dependency.join(''))) - continue; - needed.push(dependency); - } - } -} - -function record_fetched_install_dep(prefix, item, repo_url, result) -{ - const key = prefix + item; - - if (!install_target || repo_url !== install_target.repo_url || - !install_target.to_fetch.has(key)) - return; - - if (possible_errors.includes(result.state)) { - install_abort(result.state); - return; - } - - if (result.state !== "completed") - return; - - install_target.to_fetch.delete(key); - install_target.fetched.add(key); - - if (prefix === TYPE_PREFIX.BAG && - fetch_install_deps(result.response.components) === false) - return; - - install_check_ready(); -} - -function install_clicked(entry_object) -{ - import_frame.show_loading(); - - install_target = { - repo_url: entry_object.repo_url, - pattern: entry_object.match_object.pattern, - payload: entry_object.match_object.payload, - fetched: new Set(), - to_fetch: new Set() - }; - - fetch_install_deps([install_target.payload]); - - install_check_ready(); -} - -var max_query_result_id = 0; - -function show_query_successful_result(result_item, repo_url, result) -{ - if (result.length === 0) { - show_message(result_item, "No results :("); - return; - } - - const cloned_ul_template = clone_template("result_patterns_list"); - set_appended(result_item, cloned_ul_template.ul); - - for (const match of result) { - const entry_object = clone_template("query_match_li"); - - entry_object.pattern.textContent = match.pattern; - - cloned_ul_template.ul.appendChild(entry_object.li); - - if (!match.payload) { - entry_object.payload.textContent = "(none)"; - for (const key of ["chbx", "triangle", "unroll"]) - entry_object[key].remove(); - continue; - } - - entry_object.payload.textContent = nice_name(...match.payload); - - const install_cb = () => install_clicked(entry_object); - entry_object.btn.addEventListener("click", install_cb); - - const chbx_id = `query_result_${max_query_result_id++}`; - entry_object.chbx.id = chbx_id; - entry_object.lbl.setAttribute("for", chbx_id); - - entry_object.unroll_cb = () => unroll_chbx_first_checked(entry_object); - entry_object.chbx.addEventListener("change", entry_object.unroll_cb); - - entry_object.component_object = match.payload; - entry_object.match_object = match; - entry_object.repo_url = repo_url; - } -} - -function show_query_result(url_prefix, url, repo_url, result) -{ - const results_list_object = results_lists.get(url) || - create_results_list(url); - const result_item = results_list_object.by_repo.get(repo_url) || - create_result_item(results_list_object, repo_url, result); - - const completed_cb = - item => show_query_successful_result(item, repo_url, result.response); - const possible_actions = { - completed: completed_cb, - started: showcb("loading..."), - connection_error: showcb("Error when querying repository."), - parse_error: showcb("Bad data format received.") - }; - possible_actions[result.state](result_item, repo_url); -} - -by_id("settings_but") - .addEventListener("click", (e) => browser.runtime.openOptionsPage()); - -async function main() -{ - storage = await get_remote_storage(); - import_frame = await get_import_frame(); - import_frame.onclose = () => show_queried_view_radio.checked = true; - show_page_activity_info(); -} - -main(); diff --git a/html/import_frame.html b/html/import_frame.html deleted file mode 100644 index bac98a8..0000000 --- a/html/import_frame.html +++ /dev/null @@ -1,79 +0,0 @@ -<!-- - SPDX-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 - - Scripts import dialog - - This file is part of Haketilo. - - Copyright (C) 2021 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. - --> - -<style> - .padding_right { - padding-right: 0.3em; - } -</style> -<template> - <tr id="import_entry" class="nowrap" data-template="entry"> - <td> - <input type="checkbox" style="display: inline;" checked data-template="chbx"></input> - <span data-template="name_span"></span> - </td> - <td class="bold padding_right" data-template="warning"></td> - </tr> -</template> - -<input id="import_loading_radio" type="radio" name="import_window_content" class="show_next"></input> -<span> Loading... </span> - -<input id="import_failed_radio" type="radio" name="import_window_content" class="show_next"></input> -<div> - <span id="import_errormsg"></span> - <input id="import_errordetail_chbx" type="checkbox" class="show_next"></input> - <pre id="import_errordetail"></pre> - <button id="import_failok_but"> OK </button> -</div> - -<input id="import_selection_radio" type="radio" name="import_window_content" class="show_next"></input> -<div> - <button id="check_all_import_but"> Check all </button> - <button id="uncheck_all_import_but"> Uncheck all </button> - <button id="uncheck_colliding_import_but"> Uncheck existing </button> - <aside id="existing_settings_note"> - Settings that would owerwrite existing ones are marked "!". - </aside> - <div id="import_table_wrapper" class="table_wrapper"> - <div> - <table> - <tbody id="import_list"> - </tbody> - </table> - </div> - </div> - <button id="commit_import_but"> OK </button> - <button id="cancel_import_but"> Cancel </button> -</div> diff --git a/html/import_frame.js b/html/import_frame.js deleted file mode 100644 index 659d420..0000000 --- a/html/import_frame.js +++ /dev/null @@ -1,185 +0,0 @@ -/** - * This file is part of Haketilo. - * - * Function: Logic for the settings import frame. - * - * 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 <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. - */ - -#FROM common/storage_client.js IMPORT get_remote_storage -#FROM html/DOM_helpers.js IMPORT by_id, clone_template -#FROM common/misc.js IMPORT nice_name -#FROM common/once.js IMPORT make_once - -let storage; - -let import_list = by_id("import_list"); -let import_chbxs_colliding = undefined; -let entry_objects = undefined; -let settings_import_map = undefined; - -function add_import_entry(prefix, name) -{ - const cloned_template = clone_template("import_entry"); - Object.assign(cloned_template, {prefix, name}); - - cloned_template.name_span.textContent = nice_name(prefix, name); - - if (storage.get(prefix, name) !== undefined) { - import_chbxs_colliding.push(cloned_template.chbx); - cloned_template.warning.textContent = "!"; - } - - import_list.appendChild(cloned_template.entry); - - return cloned_template; -} - -function check_all_imports() -{ - for (const entry_object of entry_objects) - entry_object.chbx.checked = true; -} - -function uncheck_all_imports() -{ - for (const entry_object of entry_objects) - entry_object.chbx.checked = false; -} - -function uncheck_colliding_imports() -{ - for (let chbx of import_chbxs_colliding) - chbx.checked = false; -} - -function commit_import() -{ - for (const entry_object of entry_objects) { - if (!entry_object.chbx.checked) - continue; - - const key = entry_object.prefix + entry_object.name; - const value = settings_import_map.get(key); - storage.set(entry_object.prefix, entry_object.name, value); - } - - deactivate(); -} - -const import_loading_radio = by_id("import_loading_radio"); - -function show_loading() -{ - import_loading_radio.checked = true; -} - -const import_failed_radio = by_id("import_failed_radio"); -const import_errormsg = by_id("import_errormsg"); -const import_errordetail_chbx = by_id("import_errordetail_chbx"); -const import_errordetail = by_id("import_errordetail"); - -function show_error(errormsg, errordetail) -{ - import_failed_radio.checked = true; - import_errormsg.textContent = errormsg; - import_errordetail_chbx.checked = errordetail; - import_errordetail.textContent = errordetail; -} - -const import_selection_radio = by_id("import_selection_radio"); -const existing_settings_note = by_id("existing_settings_note"); - -function show_selection(settings) -{ - import_selection_radio.checked = true; - - let old_children = import_list.children; - while (old_children[0] !== undefined) - import_list.removeChild(old_children[0]); - - import_chbxs_colliding = []; - entry_objects = []; - 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); - entry_objects.push(add_import_entry(prefix, name)); - settings_import_map.set(key, value); - } - - const op = import_chbxs_colliding.length > 0 ? "remove" : "add"; - existing_settings_note.classList[op]("hide"); -} - -function deactivate() -{ - /* Let GC free some memory */ - import_chbxs_colliding = undefined; - entry_objects = undefined; - settings_import_map = undefined; - - if (exports.onclose) - exports.onclose(); -} - -const wrapper = by_id("import_table_wrapper"); -const style_table = (...cls) => cls.forEach(c => wrapper.classList.add(c)); - -const exports = - {show_loading, show_error, show_selection, deactivate, style_table}; - -async function init() -{ - storage = await get_remote_storage(); - - by_id("commit_import_but").addEventListener("click", commit_import); - by_id("check_all_import_but").addEventListener("click", check_all_imports); - by_id("uncheck_all_import_but") - .addEventListener("click", uncheck_all_imports); - by_id("uncheck_colliding_import_but") - .addEventListener("click", uncheck_colliding_imports); - by_id("cancel_import_but").addEventListener("click", deactivate); - by_id("import_failok_but").addEventListener("click", deactivate); - - return exports; -} - -#EXPORT make_once(init) AS get_import_frame diff --git a/html/mozilla_scrollbar_fix.css b/html/mozilla_scrollbar_fix.css deleted file mode 100644 index 08cdc93..0000000 --- a/html/mozilla_scrollbar_fix.css +++ /dev/null @@ -1,67 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 - * - * Hacky fix for vertical scrollbar width being included in child's width - * - * This file is part of Haketilo. - * - * Copyright (C) 2021 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. - */ - -/* - * Under Mozilla browsers to avoid vertical scrollbar forcing horizontal - * scrollbar to appear in an element we add the `firefox_scrollbars_hacky_fix' - * class to an element for which width has to be reserved. - * - * This is a bit hacky and relies on some assumed width of Firefox scrollbar, I - * know. And must be excluded from Chromium builds. - * - * I came up with this hack when working on popup. Before that I had the - * scrollbar issue with tables in the options page and gave up there and made - * the scrollbal always visible. Now we could try applying this "fix" there, - * too! - */ - -.firefox_scrollbars_hacky_fix { - font-size: 0; -} - -.firefox_scrollbars_hacky_fix>div { - display: inline-block; - width: -moz-available; -} - -.firefox_scrollbars_hacky_fix>*>* { - font-size: initial; -} - -.firefox_scrollbars_hacky_fix::after { - content: ""; - display: inline-block; - visibility: hidden; - font-size: initial; - width: 14px; -} - -.firefox_scrollbars_hacky_fix.has_inline_content::after { - width: calc(14px - 0.3em); -} diff --git a/html/options.html b/html/options.html deleted file mode 100644 index e317032..0000000 --- a/html/options.html +++ /dev/null @@ -1,416 +0,0 @@ -<!DOCTYPE html> -<!-- - SPDX-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 - - Extension's settings page - - This file is part of Haketilo. - - Copyright (C) 2021 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. - --> -<html> - <head> - <meta charset="utf-8"/> - <title>Haketilo options</title> -#LOADCSS html/reset.css -#LOADCSS html/base.css -#LOADCSS html/table.css - <style> - body { - width: 100%; - } - - /* tabbed view */ - #show_repos:not(:checked) ~ #repos, - #show_pages:not(:checked) ~ #pages, - #show_bags:not(:checked) ~ #bags, - #show_scripts:not(:checked) ~ #scripts { - display: none; - } - - #show_repos:checked ~ div #repos_lbl, - #show_pages:checked ~ div #pages_lbl, - #show_bags:checked ~ div #bags_lbl, - #show_scripts:checked ~ div #scripts_lbl { - background: #4CAF50; - color: white; - } - - #tab_heads>* { - font-size: 130%; - padding: 10px; - display: inline-block; - cursor: pointer; - } - - #tab_heads { - -moz-user-select: none; - user-select: none; - } - - #import_but { - font: unset; - font-size: 130%; - float: right; - margin: 0; - border-radius: 0; - } - - div.tab { - min-width: 50vw; - width: fit-content; - padding-left: 6px; - } - - /* popup window with list of selectable components */ - .popup { - position: fixed; - width: 100vw; - height: 100vh; - left: 0; - top: 0; - background-color: rgba(60,60,60,0.4); - z-index: 1; - overflow: auto; - vertical-align: center; - horizontal-align: center; - } - - .popup_frame { - background-color: #f0f0f0; - margin: 5vh auto; - padding: 15px; - border: solid #333 4px; - border-radius: 15px; - width: -moz-fit-content; - width: fit-content; - } - - .work_li .table_wrapper::before { - background: linear-gradient(#e0f0f0, #555); - } - - .work_li .table_wrapper::after { - background: linear-gradient(#555, #e0f0f0); - } - - .table_wrapper.always_scrollbar>* { - border-left: solid #454 8px; - max-height: 80vh; - overflow-y: scroll; - } - - .table_wrapper .table_wrapper.always_scrollbar>*, - .popup_frame .table_wrapper.always_scrollbar>* { - max-height: 60vh; - } - - .popup_frame .table_wrapper table { - min-width: 30vw; - } - - .popup_frame .table_wrapper { - margin: 0 auto; - } - - td:first-child { - max-width: 70vw; - overflow: hidden; - } - - tr.work_li>td:first-child { - padding-right: 0; - max-width: unset; - } - - tr.work_li>td>div { - background-color: #e0f0f0; - border-left: solid #454 5px; - border-right: solid #454 2px; - padding: 5px 10px; - } - - .form_grid { - display: grid; - grid-template-columns: auto auto; - } - - .form_grid>label { - grid-column: 1 / span 1; - margin-right: 10px; - } - - .form_grid label { - line-height: 34px; /* center text vertically */ - } - - .form_grid>input, .form_grid>span { - grid-column: 2 / span 1; - } - - .form_grid>label[for="script_contents_field"], - .form_grid>* { - grid-column: 1 / span 2; - } - - .form_grid>textarea { - min-width: 70vw; - resize: none; - } - - .form_disabled>* { - opacity: 0.5; - pointer-events: none; - } - - .form_disabled_msg { - display: none; - font-style: italic; - } - - .form_disabled .form_disabled_msg { - opacity: initial; - pointer-events: initial; - display: initial; - } - </style> - </head> - <body> - <template> - <tr id="item_li" class="nowrap"> - <td></td> - <td><div class="button"> Edit </div></td> - <td><div class="button"> Remove </div></td> - <td><div class="button"> Export </div></td> - </tr> - <tr id="bag_component_li" class="nowrap"> - <td></td> - <td><div class="button"> Remove </div></td> - </tr> - <tr id="chbx_component_li" class="nowrap"> - <td> - <input type="checkbox" style="display: inline;"></input> - <span></span> - </td> - </tr> - <tr id="radio_component_li" class="nowrap"> - <td> - <input type="radio" style="display: inline;" name="page_components"></input> - <span></span> - </td> - </tr> - </template> - - <!-- Mind the show_*s ids below - their format is assumed in js code --> - <input type="radio" name="tabs" id="show_repos"></input> - <input type="radio" name="tabs" id="show_pages" checked></input> - <input type="radio" name="tabs" id="show_bags"></input> - <input type="radio" name="tabs" id="show_scripts"></input> - <div id="tab_heads" class="has_bottom_line"> - <label for="show_repos" id="repos_lbl"> Repos </label> - <label for="show_pages" id="pages_lbl"> Pages </label> - <label for="show_bags" id="bags_lbl"> Bags </label> - <label for="show_scripts" id="scripts_lbl"> Scripts </label> - <button id="import_but" style="margin-left: 40px;"> Import </button> - </div> - <div id="repos" class="tab"> - <div class="table_wrapper tight_table has_bottom_line has_upper_line"> - <div> - <table> - <tbody id="repos_ul"> - <tr id="work_repo_li" class="hide work_li"> - <td colspan="4"> - <div class="form_grid"> - <label for="repo_url_field">URL: </label> - <input id="repo_url_field"></input> - <div> - <button id="save_repo_but" type="button"> Save </button> - <button id="discard_repo_but" type="button"> Cancel </button> - </div> - </div> - </td> - </tr> - </tbody> - </table> - </div> - </div> - <button id="add_repo_but" type="button"> Add repository </button> - </div> - <div id="pages" class="tab"> - <div class="table_wrapper tight_table has_bottom_line has_upper_line"> - <div> - <table> - <tbody id="pages_ul"> - <tr id="work_page_li" class="hide work_li"> - <td colspan="4"> - <div class="form_grid"> - <label for="page_url_field">URL: </label> - <input id="page_url_field"></input> - <label>Payload: </label> - <span class="nowrap"> - <span id="page_payload"></span> - <button id="select_page_components_but"> - Choose payload - </button> - </span> - <div id="allow_native_scripts_container" class="nowrap"> - <input id="page_allow_chbx" type="checkbox" style="display: inline;"></input> - <label for="page_allow_chbx">Allow native scripts</label> - <span class="form_disabled_msg"> - (only possible when no payload is used) - </span> - </div> - <div> - <button id="save_page_but" type="button"> Save </button> - <button id="discard_page_but" type="button"> Cancel </button> - </div> - </div> - </td> - </tr> - </tbody> - </table> - </div> - </div> - <button id="add_page_but" type="button"> Add page </button> - <br/> -#INCLUDE html/default_blocking_policy.html - </div> - <div id="bags" class="tab"> - <div class="table_wrapper tight_table has_bottom_line has_upper_line"> - <div> - <table> - <tbody id="bags_ul"> - <tr id="work_bag_li" class="hide work_li"> - <td colspan="4"> - <div class="form_grid"> - <label for="bag_name_field"> Name: </label> - <input id="bag_name_field"></input> - <div class="table_wrapper tight_table has_bottom_line has_upper_line"> - <div> - <table> - <tbody id="bag_components_ul"> - <tr id="empty_bag_component_li" class="hide"></tr> - </tbody> - </table> - </div> - </div> - <div> - <button id="select_bag_components_but"> - Add scripts - </button> - </div> - <div> - <button id="save_bag_but"> Save </button> - <button id="discard_bag_but"> Cancel </button> - </div> - </div> - </td> - </tr> - </tbody> - </table> - </div> - </div> - <button id="add_bag_but" type="button"> Add bag </button> - </div> - <div id="scripts" class="tab"> - <div class="table_wrapper tight_table has_bottom_line has_upper_line"> - <div> - <table> - <tbody id="scripts_ul"> - <tr id="work_script_li" class="hide work_li"> - <td colspan="4"> - <div class="form_grid"> - <label for="script_name_field"> Name: </label> - <input id="script_name_field"></input> - <label for="script_url_field"> URL: </label> - <input id="script_url_field"></input> - <label for="script_sha256_field"> SHA256: </label> - <input id="script_sha256_field"></input> - <aside> - Note: URL and SHA256 are ignored if script text is provided. - </aside> - <label for="script_contents_field"> contents: </label> - <textarea id="script_contents_field" rows="20" cols="80"></textarea> - <div> - <button id="save_script_but"> Save </button> - <button id="discard_script_but"> Cancel </button> - </div> - </div> - </td> - </tr> - </tbody> - </table> - </div> - </div> - <button id="add_script_but" type="button"> Add script </button> - </div> - - <div id="chbx_components_window" class="hide popup" position="absolute"> - <div class="popup_frame"> - <div class="table_wrapper tight_table has_bottom_line has_upper_line"> - <div> - <table> - <tbody id="chbx_components_ul"> - </tbody> - </table> - </div> - </div> - <button id="commit_bag_components_but"> Add </button> - <button id="cancel_bag_components_but"> Cancel </button> - </div> - </div> - - <div id="radio_components_window" class="hide popup" position="absolute"> - <div class="popup_frame"> - <div class="table_wrapper tight_table always_scrollbar has_bottom_line has_upper_line"> - <div> - <table> - <tbody id="radio_components_ul"> - <tr id="radio_component_none_li"> - <td> - <input id="radio_component_none_input" type="radio" style="display: inline;" name="page_components"></input> - <span>(None)</span> - </td> - </tr> - </tbody> - </table> - </div> - </div> - <button id="commit_page_components_but"> Choose </button> - <button id="cancel_page_components_but"> Cancel </button> - </div> - </div> - - <div id="import_window" class="hide popup" position="absolute"> - <div class="popup_frame"> - <h2> Settings import </h2> -#INCLUDE html/import_frame.html - </div> - </div> - - <a id="file_downloader" class="hide"></a> - <form id="file_opener_form" style="visibility: hidden;"> - <input type="file" id="file_opener"></input> - </form> -#LOADJS html/options_main.js - </body> -</html> 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 <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. - */ - -#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 <input>. Without this, a second attempt to import the same - * file would result in "change" event not happening on <input> 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(); diff --git a/html/popup.html b/html/popup.html index ad6c258..bb30425 100644 --- a/html/popup.html +++ b/html/popup.html @@ -55,10 +55,10 @@ #info_form, #unprivileged_page_info { display: grid; grid-template-columns: auto; - text-align: center; } #info_form * { + text-align: center; white-space: nowrap; text-overflow: ellipsis; overflow-x: hidden; diff --git a/html/settings.html b/html/settings.html index ce19e55..7abb870 100644 --- a/html/settings.html +++ b/html/settings.html @@ -33,7 +33,6 @@ <title>Haketilo options</title> #LOADCSS html/reset.css #LOADCSS html/base.css -#LOADCSS html/table.css #LOADCSS html/grid.css <style> /* Style top menu items. */ diff --git a/html/table.css b/html/table.css deleted file mode 100644 index 48792c1..0000000 --- a/html/table.css +++ /dev/null @@ -1,74 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 - * - * Table styling used in some Haketilo internal HTML pages - * - * This file is part of Haketilo. - * - * Copyright (C) 2021 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. - */ - -.table_wrapper { - display: block; - background-color: #f0f0f0; - margin: 6px 0; -} - -.table_wrapper table { - border-collapse: unset; - width: 100%; -} - -.table_wrapper.tight_table, -.table_wrapper.tight_table>*, -.table_wrapper.tight_table>*>table { - width: -moz-min-content; - width: min-content; -} - -tr:nth-child(odd) { - background-color: #e5e5e5; -} - -td { - vertical-align: middle; - min-width: fit-content; - min-width: -moz-fit-content; -} - -.tight_table td { - width: 1%; -} - -td:first-child { - padding: 3px 10px 6px; -} - -.tight_table td:first-child { - width: 100%; -} - -td>div.button { - margin-right: 4px; - white-space: nowrap; - float: right; -} |