From 76b15043533d577c6b6b67bcf0115c201dd8536f Mon Sep 17 00:00:00 2001 From: Ethan Dalool Date: Tue, 15 Sep 2020 15:04:50 -0700 Subject: [PATCH] Synchronize with Etiquette. --- frontends/ycdl_flask/static/css/common.css | 21 +- frontends/ycdl_flask/static/js/common.js | 418 +++++++++++---------- frontends/ycdl_flask/static/js/spinner.js | 2 +- 3 files changed, 238 insertions(+), 203 deletions(-) diff --git a/frontends/ycdl_flask/static/css/common.css b/frontends/ycdl_flask/static/css/common.css index 7a6ef90..f4d7bf0 100644 --- a/frontends/ycdl_flask/static/css/common.css +++ b/frontends/ycdl_flask/static/css/common.css @@ -201,17 +201,26 @@ button:active #message_area { - display: flex; - flex-direction: column; - align-items: center; + display: grid; + grid-auto-flow: row; + grid-auto-rows: min-content; + grid-gap: 8px; + padding: 8px; overflow-y: auto; 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 { - width: 80%; - margin: 4px; - + padding: 2px; word-wrap: break-word; } .message_bubble * diff --git a/frontends/ycdl_flask/static/js/common.js b/frontends/ycdl_flask/static/js/common.js index 631ac9b..349eac6 100644 --- a/frontends/ycdl_flask/static/js/common.js +++ b/frontends/ycdl_flask/static/js/common.js @@ -1,12 +1,12 @@ -var common = {}; +const common = {}; common.INPUT_TYPES = new Set(["INPUT", "TEXTAREA"]); common._request = function _request(method, url, callback) { - let request = new XMLHttpRequest(); - let response = { + const request = new XMLHttpRequest(); + const response = { "completed": false, }; @@ -29,7 +29,7 @@ function _request(method, url, callback) } callback(response); }; - let asynchronous = true; + const asynchronous = true; request.open(method, url, asynchronous); return request; } @@ -61,7 +61,7 @@ function bind_box_to_button(box, button, ctrl_enter) Thanks Yaroslav Yakovlev http://stackoverflow.com/a/9343095 */ - let bound_box_hook = function(event) + const bound_box_hook = function(event) { if (event.key !== "Enter") {return;} @@ -83,9 +83,9 @@ function create_message_bubble(message_area, message_positivity, message_text, l { lifespan = 8000; } - let message = document.createElement("div"); + const message = document.createElement("div"); message.className = "message_bubble " + message_positivity; - let span = document.createElement("span"); + const span = document.createElement("span"); span.innerHTML = message_text; message.appendChild(span); message_area.appendChild(message); @@ -168,13 +168,13 @@ function entry_with_history_hook(event) common.html_to_element = function html_to_element(html) { - let template = document.createElement("template"); - template.innerHTML = html; - return template.content.firstChild; + const template = document.createElement("template"); + template.innerHTML = html.trim(); + return template.content.firstElementChild; } common.init_atag_merge_params = -function init_atag_merge_params() +function init_atag_merge_params(a) { /* To create an tag where the ?parameters written on the href are merged @@ -195,42 +195,47 @@ function init_atag_merge_params() href: "?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)); - 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) { - const a_params = new URLSearchParams(a.search); - 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"); + setTimeout(() => common.init_atag_merge_params(a), 0); } } 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 @@ -262,192 +267,213 @@ function init_button_with_confirm() 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) { - button.classList.remove("button_with_confirm"); - - 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; + setTimeout(() => common.init_button_with_confirm(button), 0); } } 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 the DOM has completed loading, give it the disabled attribute and the 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) { - element.disabled = false; - element.classList.remove("enable_on_pageload"); + setTimeout(() => common.init_enable_on_pageload(element), 0); } } 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")); for (const input of inputs) { - input.addEventListener("keydown", common.entry_with_history_hook); - input.classList.remove("entry_with_history"); + setTimeout(() => common.init_entry_with_history(input), 0); } } common.init_tabbed_container = -function init_tabbed_container() +function init_tabbed_container(tabbed_container) { - let switch_tab = - function switch_tab(event) + const button_container = document.createElement("div"); + 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; - if (tab_button.classList.contains("tab_button_active")) - { return; } + tab.classList.add("hidden"); + const tab_id = tab.dataset.tabId || tab.dataset.tabTitle; + tab.dataset.tabId = tab_id; + tab.style.borderTopColor = "transparent"; - let tab_id = tab_button.dataset.tabId; - let tab_buttons = tab_button.parentElement.getElementsByClassName("tab_button"); - let 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"); - } - 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"); } - } + const button = document.createElement("button"); + button.className = "tab_button tab_button_inactive"; + button.onclick = common.tabbed_container_switcher; + button.innerText = tab.dataset.tabTitle; + button.dataset.tabId = tab_id; + button_container.append(button); } + 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) { - let button_container = document.createElement("div"); - 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"; + setTimeout(() => common.init_tabbed_container(tabbed_container), 0); + } +} - let button = document.createElement("button"); - button.className = "tab_button tab_button_inactive"; - button.onclick = switch_tab; - button.innerText = tab.dataset.tabTitle; - button.dataset.tabId = tab_id; - button_container.append(button); +common.tabbed_container_switcher = +function tabbed_container_switcher(event) +{ + const tab_button = event.target; + if (tab_button.classList.contains("tab_button_active")) + { 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"); - button_container.firstElementChild.classList.remove("tab_button_inactive"); - button_container.firstElementChild.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"); } } } @@ -460,10 +486,10 @@ function refresh() common.on_pageload = function on_pageload() { - common.init_atag_merge_params(); - common.init_button_with_confirm(); - common.init_enable_on_pageload(); - common.init_entry_with_history(); - common.init_tabbed_container(); + common.init_all_atag_merge_params(); + common.init_all_button_with_confirm(); + common.init_all_enable_on_pageload(); + common.init_all_entry_with_history(); + common.init_all_tabbed_container(); } document.addEventListener("DOMContentLoaded", common.on_pageload); diff --git a/frontends/ycdl_flask/static/js/spinner.js b/frontends/ycdl_flask/static/js/spinner.js index eeacd22..bf7f075 100644 --- a/frontends/ycdl_flask/static/js/spinner.js +++ b/frontends/ycdl_flask/static/js/spinner.js @@ -1,4 +1,4 @@ -var spinner = {}; +const spinner = {}; /* In general, spinners are used for functions that launch a callback, and the