mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 14:31:08 -05:00
Add JavaScript functionality for dynamic UI updates and filtering
- 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.
This commit is contained in:
425
staticfiles/autocomplete/js/autocomplete.js
Normal file
425
staticfiles/autocomplete/js/autocomplete.js
Normal file
@@ -0,0 +1,425 @@
|
||||
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 = '';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user