Synchronize with Etiquette.

This commit is contained in:
voussoir 2020-09-15 15:04:50 -07:00
parent ec19301bcc
commit 76b1504353
3 changed files with 238 additions and 203 deletions

View file

@ -201,17 +201,26 @@ button:active
#message_area #message_area
{ {
display: flex; display: grid;
flex-direction: column; grid-auto-flow: row;
align-items: center; grid-auto-rows: min-content;
grid-gap: 8px;
padding: 8px;
overflow-y: auto; overflow-y: auto;
background-color: var(--color_transparency); background-color: var(--color_transparency);
} }
#message_area > :last-child
{
/*
For some reason, the message_area's 8px padding doesn't apply to the bottom
when the container is scrolled.
*/
margin-bottom: 8px;
}
.message_bubble .message_bubble
{ {
width: 80%; padding: 2px;
margin: 4px;
word-wrap: break-word; word-wrap: break-word;
} }
.message_bubble * .message_bubble *

View file

@ -1,12 +1,12 @@
var common = {}; const common = {};
common.INPUT_TYPES = new Set(["INPUT", "TEXTAREA"]); common.INPUT_TYPES = new Set(["INPUT", "TEXTAREA"]);
common._request = common._request =
function _request(method, url, callback) function _request(method, url, callback)
{ {
let request = new XMLHttpRequest(); const request = new XMLHttpRequest();
let response = { const response = {
"completed": false, "completed": false,
}; };
@ -29,7 +29,7 @@ function _request(method, url, callback)
} }
callback(response); callback(response);
}; };
let asynchronous = true; const asynchronous = true;
request.open(method, url, asynchronous); request.open(method, url, asynchronous);
return request; return request;
} }
@ -61,7 +61,7 @@ function bind_box_to_button(box, button, ctrl_enter)
Thanks Yaroslav Yakovlev Thanks Yaroslav Yakovlev
http://stackoverflow.com/a/9343095 http://stackoverflow.com/a/9343095
*/ */
let bound_box_hook = function(event) const bound_box_hook = function(event)
{ {
if (event.key !== "Enter") if (event.key !== "Enter")
{return;} {return;}
@ -83,9 +83,9 @@ function create_message_bubble(message_area, message_positivity, message_text, l
{ {
lifespan = 8000; lifespan = 8000;
} }
let message = document.createElement("div"); const message = document.createElement("div");
message.className = "message_bubble " + message_positivity; message.className = "message_bubble " + message_positivity;
let span = document.createElement("span"); const span = document.createElement("span");
span.innerHTML = message_text; span.innerHTML = message_text;
message.appendChild(span); message.appendChild(span);
message_area.appendChild(message); message_area.appendChild(message);
@ -168,13 +168,13 @@ function entry_with_history_hook(event)
common.html_to_element = common.html_to_element =
function html_to_element(html) function html_to_element(html)
{ {
let template = document.createElement("template"); const template = document.createElement("template");
template.innerHTML = html; template.innerHTML = html.trim();
return template.content.firstChild; return template.content.firstElementChild;
} }
common.init_atag_merge_params = common.init_atag_merge_params =
function init_atag_merge_params() function init_atag_merge_params(a)
{ {
/* /*
To create an <a> tag where the ?parameters written on the href are merged To create an <a> tag where the ?parameters written on the href are merged
@ -195,42 +195,47 @@ function init_atag_merge_params()
href: "?orderby=date" href: "?orderby=date"
Result: "?filter=hello&orderby=date" Result: "?filter=hello&orderby=date"
*/ */
function ingest(params, new_params)
{
for (const [key, value] of params) { new_params.set(key, value); }
}
const page_params = Array.from(new URLSearchParams(window.location.search)); const page_params = Array.from(new URLSearchParams(window.location.search));
let as = Array.from(document.getElementsByClassName("merge_params")); let to_merge;
if (a.dataset.mergeParams)
{
const keep = new Set(a.dataset.mergeParams.split(/[\s,]+/));
to_merge = page_params.filter(key_value => keep.has(key_value[0]));
delete a.dataset.mergeParams;
}
else if (a.dataset.mergeParamsExcept)
{
const remove = new Set(a.dataset.mergeParamsExcept.split(/[\s,]+/));
to_merge = page_params.filter(key_value => (! remove.has(key_value[0])));
delete a.dataset.mergeParamsExcept;
}
else
{
to_merge = page_params;
}
to_merge = to_merge.concat(Array.from(new URLSearchParams(a.search)));
const new_params = new URLSearchParams();
for (const [key, value] of to_merge)
{ new_params.set(key, value); }
a.search = new_params.toString();
a.classList.remove("merge_params");
}
common.init_all_atag_merge_params =
function init_all_atag_merge_params()
{
const page_params = Array.from(new URLSearchParams(window.location.search));
const as = Array.from(document.getElementsByClassName("merge_params"));
for (const a of as) for (const a of as)
{ {
const a_params = new URLSearchParams(a.search); setTimeout(() => common.init_atag_merge_params(a), 0);
const new_params = new URLSearchParams();
if (a.dataset.mergeParams)
{
let keep = new Set(a.dataset.mergeParams.split(/[\s,]+/));
ingest(page_params.filter(key_value => keep.has(key_value[0])), new_params);
delete a.dataset.mergeParams;
}
else if (a.dataset.mergeParamsExcept)
{
let remove = new Set(a.dataset.mergeParamsExcept.split(/[\s,]+/));
ingest(page_params.filter(key_value => (! remove.has(key_value[0]))), new_params);
delete a.dataset.mergeParamsExcept;
}
else
{
ingest(page_params, new_params);
}
ingest(a_params, new_params);
a.search = new_params.toString();
a.classList.remove("merge_params");
} }
} }
common.init_button_with_confirm = common.init_button_with_confirm =
function init_button_with_confirm() function init_button_with_confirm(button)
{ {
/* /*
To create a button that requires a second confirmation step, assign it the To create a button that requires a second confirmation step, assign it the
@ -262,192 +267,213 @@ function init_button_with_confirm()
data-holder-class: CSS class for the new span that holds the menu. data-holder-class: CSS class for the new span that holds the menu.
*/ */
let buttons = Array.from(document.getElementsByClassName("button_with_confirm")); button.classList.remove("button_with_confirm");
const holder = document.createElement("span");
holder.className = "confirm_holder " + (button.dataset.holderClass || "");
button.parentElement.insertBefore(holder, button);
const holder_stage1 = document.createElement("span");
holder_stage1.className = "confirm_holder_stage1";
holder_stage1.appendChild(button);
holder.appendChild(holder_stage1);
const holder_stage2 = document.createElement("span");
holder_stage2.className = "confirm_holder_stage2 hidden";
holder.appendChild(holder_stage2);
let prompt;
let input_source;
if (button.dataset.isInput)
{
prompt = document.createElement("input");
prompt.placeholder = button.dataset.prompt || "";
input_source = prompt;
}
else
{
prompt = document.createElement("span");
prompt.innerText = (button.dataset.prompt || "Are you sure?") + " ";
input_source = undefined;
}
prompt.className = button.dataset.promptClass || "";
holder_stage2.appendChild(prompt)
delete button.dataset.prompt;
delete button.dataset.promptClass;
const button_confirm = document.createElement("button");
button_confirm.innerText = (button.dataset.confirm || button.innerText).trim();
if (button.dataset.confirmClass === undefined)
{
button_confirm.className = button.className;
button_confirm.classList.remove("button_with_confirm");
}
else
{
button_confirm.className = button.dataset.confirmClass;
}
button_confirm.input_source = input_source;
holder_stage2.appendChild(button_confirm);
holder_stage2.appendChild(document.createTextNode(" "));
if (button.dataset.isInput)
{
common.bind_box_to_button(prompt, button_confirm);
}
delete button.dataset.confirm;
delete button.dataset.confirmClass;
delete button.dataset.isInput;
const button_cancel = document.createElement("button");
button_cancel.innerText = button.dataset.cancel || "Cancel";
button_cancel.className = button.dataset.cancelClass || "";
holder_stage2.appendChild(button_cancel);
delete button.dataset.cancel;
delete button.dataset.cancelClass;
// If this is stupid, let me know.
const confirm_onclick = `
let holder = event.target.parentElement.parentElement;
holder.getElementsByClassName("confirm_holder_stage1")[0].classList.remove("hidden");
holder.getElementsByClassName("confirm_holder_stage2")[0].classList.add("hidden");
` + button.dataset.onclick;
button_confirm.onclick = Function(confirm_onclick);
button.removeAttribute("onclick");
button.onclick = function(event)
{
const holder = event.target.parentElement.parentElement;
holder.getElementsByClassName("confirm_holder_stage1")[0].classList.add("hidden");
holder.getElementsByClassName("confirm_holder_stage2")[0].classList.remove("hidden");
const input = holder.getElementsByTagName("input")[0];
if (input)
{
input.focus();
}
}
button_cancel.onclick = function(event)
{
const holder = event.target.parentElement.parentElement;
holder.getElementsByClassName("confirm_holder_stage1")[0].classList.remove("hidden");
holder.getElementsByClassName("confirm_holder_stage2")[0].classList.add("hidden");
}
delete button.dataset.onclick;
}
common.init_all_button_with_confirm =
function init_all_button_with_confirm()
{
const buttons = Array.from(document.getElementsByClassName("button_with_confirm"));
for (const button of buttons) for (const button of buttons)
{ {
button.classList.remove("button_with_confirm"); setTimeout(() => common.init_button_with_confirm(button), 0);
let holder = document.createElement("span");
holder.classList.add("confirm_holder");
holder.classList.add(button.dataset.holderClass || "confirm_holder");
button.parentElement.insertBefore(holder, button);
button.parentElement.removeChild(button);
let holder_stage1 = document.createElement("span");
holder_stage1.classList.add("confirm_holder_stage1");
holder_stage1.appendChild(button);
holder.appendChild(holder_stage1);
let holder_stage2 = document.createElement("span");
holder_stage2.classList.add("confirm_holder_stage2");
holder_stage2.classList.add("hidden");
holder.appendChild(holder_stage2);
let prompt;
let input_source;
if (button.dataset.isInput)
{
prompt = document.createElement("input");
prompt.placeholder = button.dataset.prompt || "";
input_source = prompt;
}
else
{
prompt = document.createElement("span");
prompt.innerText = (button.dataset.prompt || "Are you sure?") + " ";
input_source = undefined;
}
prompt.className = button.dataset.promptClass || "";
holder_stage2.appendChild(prompt)
delete button.dataset.prompt;
delete button.dataset.promptClass;
let button_confirm = document.createElement("button");
button_confirm.innerText = (button.dataset.confirm || button.innerText).trim();
if (button.dataset.confirmClass === undefined)
{
button_confirm.className = button.className;
button_confirm.classList.remove("button_with_confirm");
}
else
{
button_confirm.className = button.dataset.confirmClass;
}
button_confirm.input_source = input_source;
holder_stage2.appendChild(button_confirm);
holder_stage2.appendChild(document.createTextNode(" "));
if (button.dataset.isInput)
{
common.bind_box_to_button(prompt, button_confirm);
}
delete button.dataset.confirm;
delete button.dataset.confirmClass;
delete button.dataset.isInput;
let button_cancel = document.createElement("button");
button_cancel.innerText = button.dataset.cancel || "Cancel";
button_cancel.className = button.dataset.cancelClass || "";
holder_stage2.appendChild(button_cancel);
delete button.dataset.cancel;
delete button.dataset.cancelClass;
// If this is stupid, let me know.
let confirm_onclick = button.dataset.onclick + `
;
let holder = event.target.parentElement.parentElement;
holder.getElementsByClassName("confirm_holder_stage1")[0].classList.remove("hidden");
holder.getElementsByClassName("confirm_holder_stage2")[0].classList.add("hidden");
`
button_confirm.onclick = Function(confirm_onclick);
button.removeAttribute("onclick");
button.onclick = function(event)
{
let holder = event.target.parentElement.parentElement;
holder.getElementsByClassName("confirm_holder_stage1")[0].classList.add("hidden");
holder.getElementsByClassName("confirm_holder_stage2")[0].classList.remove("hidden");
let input = holder.getElementsByTagName("input")[0];
if (input)
{
input.focus();
}
}
button_cancel.onclick = function(event)
{
let holder = event.target.parentElement.parentElement;
holder.getElementsByClassName("confirm_holder_stage1")[0].classList.remove("hidden");
holder.getElementsByClassName("confirm_holder_stage2")[0].classList.add("hidden");
}
delete button.dataset.onclick;
} }
} }
common.init_enable_on_pageload = common.init_enable_on_pageload =
function init_enable_on_pageload() function init_enable_on_pageload(element)
{ {
/* /*
To create an input element which is disabled at first, and is enabled when To create an input element which is disabled at first, and is enabled when
the DOM has completed loading, give it the disabled attribute and the the DOM has completed loading, give it the disabled attribute and the
class "enable_on_pageload". class "enable_on_pageload".
*/ */
let elements = Array.from(document.getElementsByClassName("enable_on_pageload")); element.disabled = false;
element.classList.remove("enable_on_pageload");
}
common.init_all_enable_on_pageload =
function init_all_enable_on_pageload()
{
const elements = Array.from(document.getElementsByClassName("enable_on_pageload"));
for (const element of elements) for (const element of elements)
{ {
element.disabled = false; setTimeout(() => common.init_enable_on_pageload(element), 0);
element.classList.remove("enable_on_pageload");
} }
} }
common.init_entry_with_history = common.init_entry_with_history =
function init_entry_with_history() function init_entry_with_history(input)
{
input.addEventListener("keydown", common.entry_with_history_hook);
input.classList.remove("entry_with_history");
}
common.init_all_entry_with_history =
function init_all_entry_with_history()
{ {
const inputs = Array.from(document.getElementsByClassName("entry_with_history")); const inputs = Array.from(document.getElementsByClassName("entry_with_history"));
for (const input of inputs) for (const input of inputs)
{ {
input.addEventListener("keydown", common.entry_with_history_hook); setTimeout(() => common.init_entry_with_history(input), 0);
input.classList.remove("entry_with_history");
} }
} }
common.init_tabbed_container = common.init_tabbed_container =
function init_tabbed_container() function init_tabbed_container(tabbed_container)
{ {
let switch_tab = const button_container = document.createElement("div");
function switch_tab(event) button_container.className = "tab_buttons";
tabbed_container.prepend(button_container);
const tabs = Array.from(tabbed_container.getElementsByClassName("tab"));
for (const tab of tabs)
{ {
let tab_button = event.target; tab.classList.add("hidden");
if (tab_button.classList.contains("tab_button_active")) const tab_id = tab.dataset.tabId || tab.dataset.tabTitle;
{ return; } tab.dataset.tabId = tab_id;
tab.style.borderTopColor = "transparent";
let tab_id = tab_button.dataset.tabId; const button = document.createElement("button");
let tab_buttons = tab_button.parentElement.getElementsByClassName("tab_button"); button.className = "tab_button tab_button_inactive";
let tabs = tab_button.parentElement.parentElement.getElementsByClassName("tab"); button.onclick = common.tabbed_container_switcher;
for (const tab_button of tab_buttons) button.innerText = tab.dataset.tabTitle;
{ button.dataset.tabId = tab_id;
if (tab_button.dataset.tabId === tab_id) button_container.append(button);
{
tab_button.classList.remove("tab_button_inactive");
tab_button.classList.add("tab_button_active");
}
else
{
tab_button.classList.remove("tab_button_active");
tab_button.classList.add("tab_button_inactive");
}
}
for (const tab of tabs)
{
if (tab.dataset.tabId === tab_id)
{ tab.classList.remove("hidden"); }
else
{ tab.classList.add("hidden"); }
}
} }
tabs[0].classList.remove("hidden");
button_container.firstElementChild.classList.remove("tab_button_inactive");
button_container.firstElementChild.classList.add("tab_button_active");
}
let tabbed_containers = Array.from(document.getElementsByClassName("tabbed_container")); common.init_all_tabbed_container =
function init_all_tabbed_container()
{
const tabbed_containers = Array.from(document.getElementsByClassName("tabbed_container"));
for (const tabbed_container of tabbed_containers) for (const tabbed_container of tabbed_containers)
{ {
let button_container = document.createElement("div"); setTimeout(() => common.init_tabbed_container(tabbed_container), 0);
button_container.className = "tab_buttons"; }
tabbed_container.prepend(button_container); }
let tabs = Array.from(tabbed_container.getElementsByClassName("tab"));
for (const tab of tabs)
{
tab.classList.add("hidden");
let tab_id = tab.dataset.tabId || tab.dataset.tabTitle;
tab.dataset.tabId = tab_id;
tab.style.borderTopColor = "transparent";
let button = document.createElement("button"); common.tabbed_container_switcher =
button.className = "tab_button tab_button_inactive"; function tabbed_container_switcher(event)
button.onclick = switch_tab; {
button.innerText = tab.dataset.tabTitle; const tab_button = event.target;
button.dataset.tabId = tab_id; if (tab_button.classList.contains("tab_button_active"))
button_container.append(button); { return; }
const tab_id = tab_button.dataset.tabId;
const tab_buttons = tab_button.parentElement.getElementsByClassName("tab_button");
const tabs = tab_button.parentElement.parentElement.getElementsByClassName("tab");
for (const tab_button of tab_buttons)
{
if (tab_button.dataset.tabId === tab_id)
{
tab_button.classList.remove("tab_button_inactive");
tab_button.classList.add("tab_button_active");
} }
tabs[0].classList.remove("hidden"); else
button_container.firstElementChild.classList.remove("tab_button_inactive"); {
button_container.firstElementChild.classList.add("tab_button_active"); tab_button.classList.remove("tab_button_active");
tab_button.classList.add("tab_button_inactive");
}
}
for (const tab of tabs)
{
if (tab.dataset.tabId === tab_id)
{ tab.classList.remove("hidden"); }
else
{ tab.classList.add("hidden"); }
} }
} }
@ -460,10 +486,10 @@ function refresh()
common.on_pageload = common.on_pageload =
function on_pageload() function on_pageload()
{ {
common.init_atag_merge_params(); common.init_all_atag_merge_params();
common.init_button_with_confirm(); common.init_all_button_with_confirm();
common.init_enable_on_pageload(); common.init_all_enable_on_pageload();
common.init_entry_with_history(); common.init_all_entry_with_history();
common.init_tabbed_container(); common.init_all_tabbed_container();
} }
document.addEventListener("DOMContentLoaded", common.on_pageload); document.addEventListener("DOMContentLoaded", common.on_pageload);

View file

@ -1,4 +1,4 @@
var spinner = {}; const spinner = {};
/* /*
In general, spinners are used for functions that launch a callback, and the In general, spinners are used for functions that launch a callback, and the