mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 08:51:09 -05:00
- Implemented font color configuration based on numeric values in various sections. - Added resizing functionality for input fields to accommodate text length. - Initialized filters on document ready for improved user interaction. - Created visualization for profile data using fetched dot format. - Enhanced SQL detail page with click event handling for row navigation. - Ensured consistent highlighting for code blocks across multiple pages.
425 lines
16 KiB
JavaScript
425 lines
16 KiB
JavaScript
function phac_aspc_autocomplete_trigger_change(container_id) {
|
|
setTimeout(() => {
|
|
const container = document.getElementById(container_id);
|
|
const el = container.querySelector('.textinput');
|
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
}, 0)
|
|
}
|
|
|
|
function phac_aspc_autocomplete_clear_focus(container, activate_ring) {
|
|
const hasFocus = container.querySelectorAll('.hasFocus');
|
|
for (const el of hasFocus) {
|
|
el.classList.remove('hasFocus');
|
|
}
|
|
|
|
const el = container.querySelector('.textinput');
|
|
el.removeAttribute('aria-activedescendant');
|
|
|
|
if (activate_ring) {
|
|
container.closest('.phac_aspc_form_autocomplete_focus_ring')
|
|
.classList.add('active');
|
|
container.querySelector('.textinput').focus();
|
|
} else {
|
|
container.closest('.phac_aspc_form_autocomplete_focus_ring')
|
|
.classList.remove('active');
|
|
}
|
|
}
|
|
|
|
function phac_aspc_autocomplete_hide_results(container) {
|
|
const results = container.querySelector('.results');
|
|
const el = container.querySelector('.textinput');
|
|
el.setAttribute('aria-expanded', false);
|
|
results.classList.remove('show');
|
|
}
|
|
|
|
phac_aspc_autocomplete_blur_skip = {}
|
|
function phac_aspc_autocomplete_blur_handler(event, name, sync = false, item = false) {
|
|
// Handler responsible for blur events
|
|
// Will remove the results when focus is no longer on the component, and update
|
|
// the <input> box value when multiselect is false
|
|
requestAnimationFrame(function () {
|
|
const parent = document.getElementById(`${name}__container`);
|
|
const id = parent.getAttribute('id');
|
|
if (phac_aspc_autocomplete_blur_skip[id]) return false;
|
|
if (!parent.contains(document.activeElement)) {
|
|
// Focus has left the component
|
|
|
|
// Reset the component's state
|
|
phac_aspc_autocomplete_closed[id] = false;
|
|
if (phac_aspc_autocomplete_keyup_debounce[id]) {
|
|
clearTimeout(phac_aspc_autocomplete_keyup_debounce[id])
|
|
phac_aspc_autocomplete_keyup_debounce[id] = false;
|
|
}
|
|
|
|
// Get reference to <input> box
|
|
const el = document.getElementById(name + '__textinput');
|
|
|
|
// Abort active HTMX operations on the input box to avoid race conditions
|
|
htmx.trigger(el, 'htmx:abort');
|
|
|
|
// Set the text input value
|
|
const data_el = document.getElementById(name + '__data');
|
|
if (!sync) {
|
|
el.value = '';
|
|
} else {
|
|
el.value = data_el.getAttribute('data-phac-aspc-autocomplete');
|
|
}
|
|
phac_aspc_autocomplete_set_initial_value(parent, true);
|
|
|
|
// Get reference to list of results
|
|
const results = document.getElementById(name + '__items');
|
|
|
|
// Get reference to aria live area
|
|
const live = document.getElementById(name + '__info');
|
|
|
|
// Test if HTMX is currently in the process of swapping
|
|
if (results.classList.contains('htmx-swapping')) {
|
|
// To ensure the results are hidden, wait for HTMX to finish, then hide.
|
|
results.addEventListener(
|
|
'htmx:afterSettle', () => {
|
|
phac_aspc_autocomplete_hide_results(parent);
|
|
}
|
|
);
|
|
}
|
|
// Hide the results
|
|
phac_aspc_autocomplete_hide_results(parent);
|
|
|
|
// Clear the live info
|
|
live.innerHTML = '';
|
|
|
|
// Change the min-width of the text input back to the (small) default
|
|
parent.querySelector('.textinput')
|
|
.parentElement.classList.remove('ac-active');
|
|
|
|
// Ensure no elements remain 'focused', and set focus to input
|
|
phac_aspc_autocomplete_clear_focus(parent, item);
|
|
}
|
|
});
|
|
}
|
|
|
|
function phac_aspc_autocomplete_item_click_handler(event) {
|
|
const container = event.target.closest('.phac_aspc_form_autocomplete');
|
|
const results = container.querySelector('.results');
|
|
const open = results && results.classList.contains('show');
|
|
if (open) {
|
|
phac_aspc_autocomplete_clear_focus(container, true);
|
|
phac_aspc_autocomplete_hide_results(container);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function phac_aspc_autocomplete_focus_handler(event) {
|
|
const container = event.target.closest('.phac_aspc_form_autocomplete');
|
|
phac_aspc_autocomplete_clear_focus(container, true);
|
|
phac_aspc_autocomplete_set_initial_value(container);
|
|
setTimeout(() => {
|
|
// Announce selected items to screen readers. (if any)
|
|
const info = container.querySelector('.live_info');
|
|
info.innerHTML += ' ';
|
|
}, 100);
|
|
}
|
|
|
|
const phac_aspc_autocomplete_initial_value = {};
|
|
function phac_aspc_autocomplete_set_initial_value(container, reset = false) {
|
|
const id = container.getAttribute('id');
|
|
const el = container.querySelector('.textinput');
|
|
if (reset) {
|
|
phac_aspc_autocomplete_initial_value[id] = undefined;
|
|
return;
|
|
}
|
|
if (phac_aspc_autocomplete_initial_value[id] === undefined) {
|
|
phac_aspc_autocomplete_initial_value[id] = el.value;
|
|
}
|
|
}
|
|
|
|
phac_aspc_autocomplete_closed = {};
|
|
function phac_aspc_autocomplete_click_handler(event) {
|
|
if (event.target.classList.contains('item')) return true;
|
|
const container = event.target.closest('.phac_aspc_form_autocomplete');
|
|
const id = container.getAttribute('id');
|
|
const results = container.querySelector('.results');
|
|
const open = results && results.classList.contains('show');
|
|
const text_box = container.querySelector('.textinput');
|
|
|
|
phac_aspc_autocomplete_set_initial_value(container);
|
|
phac_aspc_autocomplete_clear_focus(container, true);
|
|
|
|
phac_aspc_autocomplete_closed[id] = open;
|
|
if (open) {
|
|
phac_aspc_autocomplete_hide_results(container);
|
|
} else {
|
|
text_box.dispatchEvent(new Event('phac_aspc_autocomplete_trigger'));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const phac_aspc_autocomplete_keyup_debounce = {};
|
|
function phac_aspc_autocomplete_keyup_handler(event) {
|
|
if (event.keyCode === 13) return false;
|
|
const debounce = phac_aspc_autocomplete_keyup_debounce;
|
|
const value = phac_aspc_autocomplete_initial_value;
|
|
const elem = event.target;
|
|
|
|
const container = elem.closest('.phac_aspc_form_autocomplete');
|
|
const id = container.getAttribute('id');
|
|
|
|
phac_aspc_autocomplete_set_initial_value(container);
|
|
|
|
if (debounce[id]) {
|
|
clearTimeout(debounce[id]);
|
|
debounce[id] = false;
|
|
}
|
|
|
|
const v = elem.value;
|
|
|
|
debounce[id] = setTimeout(() => {
|
|
if (!phac_aspc_autocomplete_closed[id] && v != value[id]) {
|
|
elem.dispatchEvent(new Event('phac_aspc_autocomplete_trigger'));
|
|
} else if (
|
|
phac_aspc_autocomplete_closed[id] &&
|
|
v != value[id] &&
|
|
v == ''
|
|
) {
|
|
phac_aspc_autocomplete_closed[id] = false;
|
|
}
|
|
value[id] = v;
|
|
}, 250);
|
|
return true;
|
|
}
|
|
|
|
const phac_aspc_autocomplete_keydown_debounce = {};
|
|
function phac_aspc_autocomplete_keydown_handler(event) {
|
|
if (event.target.classList.contains('textinput') && event.keyCode > 47) {
|
|
// Expands the min-width of text input to a reasonable size when typing
|
|
event.target.parentElement.classList.add('ac-active');
|
|
} else if (event.target.classList.contains('textinput') && event.keyCode === 8
|
|
&& event.target.value.length === 1) {
|
|
// Shrinks the min-width of text input back to the (small) default if
|
|
// the text input is empty due to backspacing
|
|
event.target.parentElement.classList.remove('ac-active');
|
|
}
|
|
// Handler responsible for keyboard navigation (up, down, esc and backspace)
|
|
const debounce = phac_aspc_autocomplete_keydown_debounce;
|
|
const whereTo = (container, down = true, skip_element = true, count = 1) => {
|
|
// This function determines which element should receive focus
|
|
// TODO: bug with down
|
|
if (!container) return null;
|
|
const results = container.querySelector('.results');
|
|
let element = container.querySelector('.hasFocus');
|
|
const must_skip = Boolean(element);
|
|
const fallback = down ? results.querySelector('a:first-child')
|
|
: results.querySelector('a:last-child');
|
|
if (!element) element = fallback;
|
|
if (!element) return null;
|
|
const dir = down ?
|
|
elem => elem.nextElementSibling : elem => elem.previousElementSibling;
|
|
|
|
let el = skip_element && must_skip ? dir(element) : element;
|
|
let counter = count;
|
|
while (el && counter > 0) {
|
|
if (el.getAttribute('href')) {
|
|
if (counter === 1) return el;
|
|
}
|
|
if (counter !== 1) counter -= 1;
|
|
el = dir(el);
|
|
}
|
|
if (counter > 0) return fallback;
|
|
return null;
|
|
}
|
|
const switchFocus = (element, container) => {
|
|
phac_aspc_autocomplete_clear_focus(container);
|
|
const el = container.querySelector('.textinput');
|
|
el.setAttribute('aria-activedescendant', element.getAttribute('id'));
|
|
element.classList.add('hasFocus');
|
|
element.scrollIntoView({ block: 'nearest' })
|
|
}
|
|
const selectFocusedItem = (container) => {
|
|
const item = container.querySelector('.hasFocus');
|
|
if (item) {
|
|
item.dispatchEvent(new Event('click'));
|
|
}
|
|
return item;
|
|
}
|
|
const focusWhenResultsShown = (container, timeout, up) => {
|
|
// This function uses polling to wait for the results to be shown before
|
|
// moving focus.
|
|
const id = container.getAttribute('id');
|
|
const results = container.querySelector('.results');
|
|
if (!results || !results.classList.contains('show')) {
|
|
if (timeout > 0) {
|
|
if (debounce[id])
|
|
clearTimeout(debounce[id]);
|
|
debounce[id] =
|
|
setTimeout(
|
|
() => focusWhenResultsShown(container, timeout - 100, up),
|
|
100
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
debounce[id] = undefined;
|
|
phac_aspc_autocomplete_closed[id] = false;
|
|
if (up) {
|
|
const prev = whereTo(container, false);
|
|
if (prev) switchFocus(prev, container);
|
|
} else {
|
|
const next = whereTo(container, true, false);
|
|
if (next) switchFocus(next, container);
|
|
}
|
|
}
|
|
|
|
const getPageSize = (container) => {
|
|
const r1 = container.getBoundingClientRect();
|
|
const r2 = container.querySelector('.item').getBoundingClientRect();
|
|
return Math.floor((r1.bottom - r1.top) / (r2.bottom - r2.top));
|
|
}
|
|
|
|
const container = event.target.closest('.phac_aspc_form_autocomplete');
|
|
const results = container.querySelector('.results');
|
|
const id = container.getAttribute('id');
|
|
|
|
phac_aspc_autocomplete_set_initial_value(container);
|
|
|
|
if (event.keyCode === 27) {
|
|
// Escape key
|
|
if (results && results.classList.contains('show')) {
|
|
phac_aspc_autocomplete_clear_focus(container, true);
|
|
phac_aspc_autocomplete_hide_results(container);
|
|
phac_aspc_autocomplete_closed[id] = true;
|
|
} else {
|
|
event.target.value = '';
|
|
}
|
|
} else if (event.keyCode === 13) {
|
|
// Enter key
|
|
if (results && results.classList.contains('show')) {
|
|
selectFocusedItem(container);
|
|
phac_aspc_autocomplete_clear_focus(container, true);
|
|
phac_aspc_autocomplete_hide_results(container);
|
|
}
|
|
return false;
|
|
} else if (
|
|
event.keyCode === 8 &&
|
|
event.target.value.length === 0
|
|
) {
|
|
// Backspace key on text input
|
|
const chip = container.querySelectorAll('.chip a');
|
|
if (chip.length > 0) chip[chip.length - 1].dispatchEvent(new Event('click'));
|
|
} else if (event.keyCode === 33) {
|
|
// Page up key
|
|
if (results) {
|
|
const prev = whereTo(
|
|
container,
|
|
false,
|
|
true,
|
|
getPageSize(results)
|
|
);
|
|
if (prev) switchFocus(prev, container);
|
|
return false;
|
|
}
|
|
} else if (event.keyCode === 34) {
|
|
// Page down key
|
|
if (results) {
|
|
const next = whereTo(
|
|
container,
|
|
true,
|
|
true,
|
|
getPageSize(results)
|
|
);
|
|
if (next) switchFocus(next, container);
|
|
return false;
|
|
}
|
|
} else if (event.keyCode === 40) {
|
|
// down arrow
|
|
// Open the results if they are not shown
|
|
if (!results || !results.classList.contains('show')) {
|
|
event.target.dispatchEvent(new Event('phac_aspc_autocomplete_trigger'));
|
|
if (event.altKey) {
|
|
phac_aspc_autocomplete_closed[id] = false;
|
|
} else {
|
|
focusWhenResultsShown(container, 3000);
|
|
}
|
|
} else {
|
|
const next = whereTo(container);
|
|
if (next) switchFocus(next, container);
|
|
}
|
|
return false;
|
|
} else if (event.keyCode === 38) {
|
|
// up arrow on item
|
|
// Open the results if they are not shown
|
|
if (!results || !results.classList.contains('show')) {
|
|
event.target.dispatchEvent(new Event('phac_aspc_autocomplete_trigger'));
|
|
if (event.altKey) {
|
|
phac_aspc_autocomplete_closed[id] = false;
|
|
} else {
|
|
focusWhenResultsShown(container, 3000, true);
|
|
}
|
|
} else {
|
|
const prev = whereTo(container, false);
|
|
if (prev) switchFocus(prev, container);
|
|
}
|
|
return false;
|
|
} else {
|
|
phac_aspc_autocomplete_closed[id] = false;
|
|
}
|
|
phac_aspc_autocomplete_clear_focus(container, true);
|
|
return true;
|
|
}
|
|
|
|
class AbstractAutocompleteHelper {
|
|
/*
|
|
this is a helper class to manipulate autocomplete components
|
|
creating instances has zero side-effects
|
|
it's assumed you may instantiate the same component multiple times
|
|
*/
|
|
constructor(fieldName, componentPrefix="") {
|
|
this.fieldName = fieldName;
|
|
this.componentPrefix = componentPrefix;
|
|
}
|
|
getComponentId(){
|
|
return this.componentPrefix + this.fieldName;
|
|
}
|
|
getContainer(){
|
|
return document.getElementById(`${this.getComponentId()}__container`);
|
|
}
|
|
getInput(){
|
|
return this.getContainer().querySelector(`#${this.getComponentId()}__textinput`);
|
|
}
|
|
getInputWrapper(){
|
|
return this.getContainer().querySelector(`#${this.getComponentId()}`);
|
|
}
|
|
getResultItems(){
|
|
return this.getContainer().querySelector(`#${this.getComponentId()}__items`);
|
|
}
|
|
getInfo(){
|
|
return this.getContainer().querySelector(`#${this.getComponentId()}__info`);
|
|
}
|
|
getDataContainer(){
|
|
return this.getContainer().querySelector(`#${this.getComponentId()}__data`);
|
|
}
|
|
// behavioral methods
|
|
clear(){
|
|
this.getInput().value = '';
|
|
this.getInputWrapper().innerHTML = '';
|
|
this.getResultItems().innerHTML = '';
|
|
this.getInfo().innerHTML = '';
|
|
this.getDataContainer().removeAttribute('data-phac-aspc-autocomplete');
|
|
}
|
|
}
|
|
|
|
class SingleAutocompleteHelper extends AbstractAutocompleteHelper {}
|
|
|
|
class MultiAutocompleteHelper extends AbstractAutocompleteHelper {
|
|
getSrDescription(){
|
|
return this.getContainer().querySelector(`#${this.getComponentId()}__sr_description`);
|
|
}
|
|
|
|
getChips(){
|
|
return this.getContainer().querySelectorAll(`#${this.getComponentId()}_ac_container li.chip`);
|
|
}
|
|
|
|
clear(){
|
|
super.clear();
|
|
this.getChips().forEach(chip => chip.remove());
|
|
this.getSrDescription().innerHTML = '';
|
|
}
|
|
} |