mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-20 02:06:42 -06:00
Clean up TypeScript file structure, fix missing VLAN tag visibility logic
This commit is contained in:
parent
85b61c0b7e
commit
2e90f22529
BIN
netbox/project-static/dist/config.js
vendored
BIN
netbox/project-static/dist/config.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/config.js.map
vendored
BIN
netbox/project-static/dist/config.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/jobs.js
vendored
BIN
netbox/project-static/dist/jobs.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/jobs.js.map
vendored
BIN
netbox/project-static/dist/jobs.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/lldp.js
vendored
BIN
netbox/project-static/dist/lldp.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/lldp.js.map
vendored
BIN
netbox/project-static/dist/lldp.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/status.js
vendored
BIN
netbox/project-static/dist/status.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/status.js.map
vendored
BIN
netbox/project-static/dist/status.js.map
vendored
Binary file not shown.
@ -1,329 +0,0 @@
|
|||||||
import { createToast } from './bs';
|
|
||||||
import { setColorMode } from './colorMode';
|
|
||||||
import { objectDepthState } from './stores';
|
|
||||||
import {
|
|
||||||
slugify,
|
|
||||||
isTruthy,
|
|
||||||
apiPatch,
|
|
||||||
hasError,
|
|
||||||
getElement,
|
|
||||||
getElements,
|
|
||||||
findFirstAdjacent,
|
|
||||||
} from './util';
|
|
||||||
|
|
||||||
import type { StateManager } from './state';
|
|
||||||
|
|
||||||
type ObjectDepthState = { hidden: boolean };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When the toggle button is clicked, swap the connection status via the API and toggle CSS
|
|
||||||
* classes to reflect the connection status.
|
|
||||||
*
|
|
||||||
* @param element Connection Toggle Button Element
|
|
||||||
*/
|
|
||||||
function toggleConnection(element: HTMLButtonElement): void {
|
|
||||||
const id = element.getAttribute('data');
|
|
||||||
const connected = element.classList.contains('connected');
|
|
||||||
const status = connected ? 'planned' : 'connected';
|
|
||||||
|
|
||||||
if (isTruthy(id)) {
|
|
||||||
apiPatch(`/api/dcim/cables/${id}/`, { status }).then(res => {
|
|
||||||
if (hasError(res)) {
|
|
||||||
// If the API responds with an error, show it to the user.
|
|
||||||
createToast('danger', 'Error', res.error).show();
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
// Get the button's row to change its styles.
|
|
||||||
const row = element.parentElement?.parentElement as HTMLTableRowElement;
|
|
||||||
// Get the button's icon to change its CSS class.
|
|
||||||
const icon = element.querySelector('i.mdi, span.mdi') as HTMLSpanElement;
|
|
||||||
if (connected) {
|
|
||||||
row.classList.remove('success');
|
|
||||||
row.classList.add('info');
|
|
||||||
element.classList.remove('connected', 'btn-warning');
|
|
||||||
element.classList.add('btn-info');
|
|
||||||
element.title = 'Mark Installed';
|
|
||||||
icon.classList.remove('mdi-lan-disconnect');
|
|
||||||
icon.classList.add('mdi-lan-connect');
|
|
||||||
} else {
|
|
||||||
row.classList.remove('info');
|
|
||||||
row.classList.add('success');
|
|
||||||
element.classList.remove('btn-success');
|
|
||||||
element.classList.add('connected', 'btn-warning');
|
|
||||||
element.title = 'Mark Installed';
|
|
||||||
icon.classList.remove('mdi-lan-connect');
|
|
||||||
icon.classList.add('mdi-lan-disconnect');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function initConnectionToggle(): void {
|
|
||||||
for (const element of getElements<HTMLButtonElement>('button.cable-toggle')) {
|
|
||||||
element.addEventListener('click', () => toggleConnection(element));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change toggle button's text and attribute to reflect the current state.
|
|
||||||
*
|
|
||||||
* @param hidden `true` if the current state is hidden, `false` otherwise.
|
|
||||||
* @param button Toggle element.
|
|
||||||
*/
|
|
||||||
function toggleDepthButton(hidden: boolean, button: HTMLButtonElement): void {
|
|
||||||
button.setAttribute('data-depth-indicators', hidden ? 'hidden' : 'shown');
|
|
||||||
button.innerText = hidden ? 'Show Depth Indicators' : 'Hide Depth Indicators';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show all depth indicators.
|
|
||||||
*/
|
|
||||||
function showDepthIndicators(): void {
|
|
||||||
for (const element of getElements<HTMLDivElement>('.record-depth')) {
|
|
||||||
element.style.display = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hide all depth indicators.
|
|
||||||
*/
|
|
||||||
function hideDepthIndicators(): void {
|
|
||||||
for (const element of getElements<HTMLDivElement>('.record-depth')) {
|
|
||||||
element.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update object depth local state and visualization when the button is clicked.
|
|
||||||
*
|
|
||||||
* @param state State instance.
|
|
||||||
* @param button Toggle element.
|
|
||||||
*/
|
|
||||||
function handleDepthToggle(state: StateManager<ObjectDepthState>, button: HTMLButtonElement): void {
|
|
||||||
const initiallyHidden = state.get('hidden');
|
|
||||||
state.set('hidden', !initiallyHidden);
|
|
||||||
const hidden = state.get('hidden');
|
|
||||||
|
|
||||||
if (hidden) {
|
|
||||||
hideDepthIndicators();
|
|
||||||
} else {
|
|
||||||
showDepthIndicators();
|
|
||||||
}
|
|
||||||
toggleDepthButton(hidden, button);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize object depth toggle buttons.
|
|
||||||
*/
|
|
||||||
function initDepthToggle(): void {
|
|
||||||
const initiallyHidden = objectDepthState.get('hidden');
|
|
||||||
|
|
||||||
for (const button of getElements<HTMLButtonElement>('button.toggle-depth')) {
|
|
||||||
toggleDepthButton(initiallyHidden, button);
|
|
||||||
|
|
||||||
button.addEventListener(
|
|
||||||
'click',
|
|
||||||
event => {
|
|
||||||
handleDepthToggle(objectDepthState, event.currentTarget as HTMLButtonElement);
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Synchronize local state with default DOM elements.
|
|
||||||
if (initiallyHidden) {
|
|
||||||
hideDepthIndicators();
|
|
||||||
} else if (!initiallyHidden) {
|
|
||||||
showDepthIndicators();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If a slug field exists, add event listeners to handle automatically generating its value.
|
|
||||||
*/
|
|
||||||
function initReslug(): void {
|
|
||||||
const slugField = document.getElementById('id_slug') as HTMLInputElement;
|
|
||||||
const slugButton = document.getElementById('reslug') as HTMLButtonElement;
|
|
||||||
if (slugField === null || slugButton === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const sourceId = slugField.getAttribute('slug-source');
|
|
||||||
const sourceField = document.getElementById(`id_${sourceId}`) as HTMLInputElement;
|
|
||||||
|
|
||||||
if (sourceField === null) {
|
|
||||||
console.error('Unable to find field for slug field.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const slugLengthAttr = slugField.getAttribute('maxlength');
|
|
||||||
let slugLength = 50;
|
|
||||||
|
|
||||||
if (slugLengthAttr) {
|
|
||||||
slugLength = Number(slugLengthAttr);
|
|
||||||
}
|
|
||||||
sourceField.addEventListener('blur', () => {
|
|
||||||
slugField.value = slugify(sourceField.value, slugLength);
|
|
||||||
});
|
|
||||||
slugButton.addEventListener('click', () => {
|
|
||||||
slugField.value = slugify(sourceField.value, slugLength);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform actions in the UI based on the value of user profile updates.
|
|
||||||
*
|
|
||||||
* @param event Form Submit
|
|
||||||
*/
|
|
||||||
function handlePreferenceSave(event: Event): void {
|
|
||||||
// Create a FormData instance to access the form values.
|
|
||||||
const form = event.currentTarget as HTMLFormElement;
|
|
||||||
const formData = new FormData(form);
|
|
||||||
|
|
||||||
// Update the UI color mode immediately when the user preference changes.
|
|
||||||
if (formData.get('ui.colormode') === 'dark') {
|
|
||||||
setColorMode('dark');
|
|
||||||
} else if (formData.get('ui.colormode') === 'light') {
|
|
||||||
setColorMode('light');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize handlers for user profile updates.
|
|
||||||
*/
|
|
||||||
function initPreferenceUpdate(): void {
|
|
||||||
const form = getElement<HTMLFormElement>('preferences-update');
|
|
||||||
if (form !== null) {
|
|
||||||
form.addEventListener('submit', handlePreferenceSave);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the select all card when the select all checkbox is checked, and sync the checkbox state
|
|
||||||
* with all the PK checkboxes in the table.
|
|
||||||
*
|
|
||||||
* @param event Change Event
|
|
||||||
*/
|
|
||||||
function handleSelectAllToggle(event: Event): void {
|
|
||||||
// Select all checkbox in header row.
|
|
||||||
const tableSelectAll = event.currentTarget as HTMLInputElement;
|
|
||||||
// Nearest table to the select all checkbox.
|
|
||||||
const table = findFirstAdjacent<HTMLInputElement>(tableSelectAll, 'table');
|
|
||||||
// Select all confirmation card.
|
|
||||||
const confirmCard = document.getElementById('select-all-box');
|
|
||||||
// Checkbox in confirmation card to signal if all objects should be selected.
|
|
||||||
const confirmCheckbox = document.getElementById('select-all') as Nullable<HTMLInputElement>;
|
|
||||||
|
|
||||||
if (table !== null) {
|
|
||||||
for (const element of table.querySelectorAll<HTMLInputElement>(
|
|
||||||
'input[type="checkbox"][name="pk"]',
|
|
||||||
)) {
|
|
||||||
if (tableSelectAll.checked) {
|
|
||||||
// Check all PK checkboxes if the select all checkbox is checked.
|
|
||||||
element.checked = true;
|
|
||||||
} else {
|
|
||||||
// Uncheck all PK checkboxes if the select all checkbox is unchecked.
|
|
||||||
element.checked = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (confirmCard !== null) {
|
|
||||||
if (tableSelectAll.checked) {
|
|
||||||
// Unhide the select all confirmation card if the select all checkbox is checked.
|
|
||||||
confirmCard.classList.remove('d-none');
|
|
||||||
} else {
|
|
||||||
// Hide the select all confirmation card if the select all checkbox is unchecked.
|
|
||||||
confirmCard.classList.add('d-none');
|
|
||||||
if (confirmCheckbox !== null) {
|
|
||||||
// Uncheck the confirmation checkbox when the table checkbox is unchecked (after which
|
|
||||||
// the confirmation card will be hidden).
|
|
||||||
confirmCheckbox.checked = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If any PK checkbox is checked, uncheck the select all table checkbox and the select all
|
|
||||||
* confirmation checkbox.
|
|
||||||
*
|
|
||||||
* @param event Change Event
|
|
||||||
*/
|
|
||||||
function handlePkCheck(event: Event): void {
|
|
||||||
const target = event.currentTarget as HTMLInputElement;
|
|
||||||
if (!target.checked) {
|
|
||||||
for (const element of getElements<HTMLInputElement>(
|
|
||||||
'input[type="checkbox"].toggle',
|
|
||||||
'input#select-all',
|
|
||||||
)) {
|
|
||||||
element.checked = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Synchronize the select all confirmation checkbox state with the select all confirmation button
|
|
||||||
* disabled state. If the select all confirmation checkbox is checked, the buttons should be
|
|
||||||
* enabled. If not, the buttons should be disabled.
|
|
||||||
*
|
|
||||||
* @param event Change Event
|
|
||||||
*/
|
|
||||||
function handleSelectAll(event: Event): void {
|
|
||||||
const target = event.currentTarget as HTMLInputElement;
|
|
||||||
const selectAllBox = getElement<HTMLDivElement>('select-all-box');
|
|
||||||
if (selectAllBox !== null) {
|
|
||||||
for (const button of selectAllBox.querySelectorAll<HTMLButtonElement>(
|
|
||||||
'button[type="submit"]',
|
|
||||||
)) {
|
|
||||||
if (target.checked) {
|
|
||||||
button.disabled = false;
|
|
||||||
} else {
|
|
||||||
button.disabled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize table select all elements.
|
|
||||||
*/
|
|
||||||
function initSelectAll(): void {
|
|
||||||
for (const element of getElements<HTMLInputElement>(
|
|
||||||
'table tr th > input[type="checkbox"].toggle',
|
|
||||||
)) {
|
|
||||||
element.addEventListener('change', handleSelectAllToggle);
|
|
||||||
}
|
|
||||||
for (const element of getElements<HTMLInputElement>('input[type="checkbox"][name="pk"]')) {
|
|
||||||
element.addEventListener('change', handlePkCheck);
|
|
||||||
}
|
|
||||||
const selectAll = getElement<HTMLInputElement>('select-all');
|
|
||||||
|
|
||||||
if (selectAll !== null) {
|
|
||||||
selectAll.addEventListener('change', handleSelectAll);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePerPageSelect(event: Event): void {
|
|
||||||
const select = event.currentTarget as HTMLSelectElement;
|
|
||||||
if (select.form !== null) {
|
|
||||||
select.form.submit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function initPerPage(): void {
|
|
||||||
for (const element of getElements<HTMLSelectElement>('select.per-page')) {
|
|
||||||
element.addEventListener('change', handlePerPageSelect);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initButtons(): void {
|
|
||||||
for (const func of [
|
|
||||||
initDepthToggle,
|
|
||||||
initConnectionToggle,
|
|
||||||
initReslug,
|
|
||||||
initSelectAll,
|
|
||||||
initPreferenceUpdate,
|
|
||||||
initPerPage,
|
|
||||||
]) {
|
|
||||||
func();
|
|
||||||
}
|
|
||||||
}
|
|
52
netbox/project-static/src/buttons/connectionToggle.ts
Normal file
52
netbox/project-static/src/buttons/connectionToggle.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { createToast } from '../bs';
|
||||||
|
import { isTruthy, apiPatch, hasError, getElements } from '../util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the toggle button is clicked, swap the connection status via the API and toggle CSS
|
||||||
|
* classes to reflect the connection status.
|
||||||
|
*
|
||||||
|
* @param element Connection Toggle Button Element
|
||||||
|
*/
|
||||||
|
function toggleConnection(element: HTMLButtonElement): void {
|
||||||
|
const id = element.getAttribute('data');
|
||||||
|
const connected = element.classList.contains('connected');
|
||||||
|
const status = connected ? 'planned' : 'connected';
|
||||||
|
|
||||||
|
if (isTruthy(id)) {
|
||||||
|
apiPatch(`/api/dcim/cables/${id}/`, { status }).then(res => {
|
||||||
|
if (hasError(res)) {
|
||||||
|
// If the API responds with an error, show it to the user.
|
||||||
|
createToast('danger', 'Error', res.error).show();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// Get the button's row to change its styles.
|
||||||
|
const row = element.parentElement?.parentElement as HTMLTableRowElement;
|
||||||
|
// Get the button's icon to change its CSS class.
|
||||||
|
const icon = element.querySelector('i.mdi, span.mdi') as HTMLSpanElement;
|
||||||
|
if (connected) {
|
||||||
|
row.classList.remove('success');
|
||||||
|
row.classList.add('info');
|
||||||
|
element.classList.remove('connected', 'btn-warning');
|
||||||
|
element.classList.add('btn-info');
|
||||||
|
element.title = 'Mark Installed';
|
||||||
|
icon.classList.remove('mdi-lan-disconnect');
|
||||||
|
icon.classList.add('mdi-lan-connect');
|
||||||
|
} else {
|
||||||
|
row.classList.remove('info');
|
||||||
|
row.classList.add('success');
|
||||||
|
element.classList.remove('btn-success');
|
||||||
|
element.classList.add('connected', 'btn-warning');
|
||||||
|
element.title = 'Mark Installed';
|
||||||
|
icon.classList.remove('mdi-lan-connect');
|
||||||
|
icon.classList.add('mdi-lan-disconnect');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initConnectionToggle(): void {
|
||||||
|
for (const element of getElements<HTMLButtonElement>('button.cable-toggle')) {
|
||||||
|
element.addEventListener('click', () => toggleConnection(element));
|
||||||
|
}
|
||||||
|
}
|
79
netbox/project-static/src/buttons/depthToggle.ts
Normal file
79
netbox/project-static/src/buttons/depthToggle.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { objectDepthState } from '../stores';
|
||||||
|
import { getElements } from '../util';
|
||||||
|
|
||||||
|
import type { StateManager } from '../state';
|
||||||
|
|
||||||
|
type ObjectDepthState = { hidden: boolean };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change toggle button's text and attribute to reflect the current state.
|
||||||
|
*
|
||||||
|
* @param hidden `true` if the current state is hidden, `false` otherwise.
|
||||||
|
* @param button Toggle element.
|
||||||
|
*/
|
||||||
|
function toggleDepthButton(hidden: boolean, button: HTMLButtonElement): void {
|
||||||
|
button.setAttribute('data-depth-indicators', hidden ? 'hidden' : 'shown');
|
||||||
|
button.innerText = hidden ? 'Show Depth Indicators' : 'Hide Depth Indicators';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show all depth indicators.
|
||||||
|
*/
|
||||||
|
function showDepthIndicators(): void {
|
||||||
|
for (const element of getElements<HTMLDivElement>('.record-depth')) {
|
||||||
|
element.style.display = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide all depth indicators.
|
||||||
|
*/
|
||||||
|
function hideDepthIndicators(): void {
|
||||||
|
for (const element of getElements<HTMLDivElement>('.record-depth')) {
|
||||||
|
element.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update object depth local state and visualization when the button is clicked.
|
||||||
|
*
|
||||||
|
* @param state State instance.
|
||||||
|
* @param button Toggle element.
|
||||||
|
*/
|
||||||
|
function handleDepthToggle(state: StateManager<ObjectDepthState>, button: HTMLButtonElement): void {
|
||||||
|
const initiallyHidden = state.get('hidden');
|
||||||
|
state.set('hidden', !initiallyHidden);
|
||||||
|
const hidden = state.get('hidden');
|
||||||
|
|
||||||
|
if (hidden) {
|
||||||
|
hideDepthIndicators();
|
||||||
|
} else {
|
||||||
|
showDepthIndicators();
|
||||||
|
}
|
||||||
|
toggleDepthButton(hidden, button);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize object depth toggle buttons.
|
||||||
|
*/
|
||||||
|
export function initDepthToggle(): void {
|
||||||
|
const initiallyHidden = objectDepthState.get('hidden');
|
||||||
|
|
||||||
|
for (const button of getElements<HTMLButtonElement>('button.toggle-depth')) {
|
||||||
|
toggleDepthButton(initiallyHidden, button);
|
||||||
|
|
||||||
|
button.addEventListener(
|
||||||
|
'click',
|
||||||
|
event => {
|
||||||
|
handleDepthToggle(objectDepthState, event.currentTarget as HTMLButtonElement);
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Synchronize local state with default DOM elements.
|
||||||
|
if (initiallyHidden) {
|
||||||
|
hideDepthIndicators();
|
||||||
|
} else if (!initiallyHidden) {
|
||||||
|
showDepthIndicators();
|
||||||
|
}
|
||||||
|
}
|
21
netbox/project-static/src/buttons/index.ts
Normal file
21
netbox/project-static/src/buttons/index.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { initConnectionToggle } from './connectionToggle';
|
||||||
|
import { initDepthToggle } from './depthToggle';
|
||||||
|
import { initMoveButtons } from './moveOptions';
|
||||||
|
import { initPerPage } from './pagination';
|
||||||
|
import { initPreferenceUpdate } from './preferences';
|
||||||
|
import { initReslug } from './reslug';
|
||||||
|
import { initSelectAll } from './selectAll';
|
||||||
|
|
||||||
|
export function initButtons(): void {
|
||||||
|
for (const func of [
|
||||||
|
initDepthToggle,
|
||||||
|
initConnectionToggle,
|
||||||
|
initReslug,
|
||||||
|
initSelectAll,
|
||||||
|
initPreferenceUpdate,
|
||||||
|
initPerPage,
|
||||||
|
initMoveButtons,
|
||||||
|
]) {
|
||||||
|
func();
|
||||||
|
}
|
||||||
|
}
|
61
netbox/project-static/src/buttons/moveOptions.ts
Normal file
61
netbox/project-static/src/buttons/moveOptions.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { getElements } from '../util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move selected options of a select element up in order.
|
||||||
|
*
|
||||||
|
* Adapted from:
|
||||||
|
* @see https://www.tomred.net/css-html-js/reorder-option-elements-of-an-html-select.html
|
||||||
|
* @param element Select Element
|
||||||
|
*/
|
||||||
|
function moveOptionUp(element: HTMLSelectElement): void {
|
||||||
|
const options = Array.from(element.options);
|
||||||
|
for (let i = 1; i < options.length; i++) {
|
||||||
|
const option = options[i];
|
||||||
|
if (option.selected) {
|
||||||
|
element.removeChild(option);
|
||||||
|
element.insertBefore(option, element.options[i - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move selected options of a select element down in order.
|
||||||
|
*
|
||||||
|
* Adapted from:
|
||||||
|
* @see https://www.tomred.net/css-html-js/reorder-option-elements-of-an-html-select.html
|
||||||
|
* @param element Select Element
|
||||||
|
*/
|
||||||
|
function moveOptionDown(element: HTMLSelectElement): void {
|
||||||
|
const options = Array.from(element.options);
|
||||||
|
for (let i = options.length - 2; i >= 0; i--) {
|
||||||
|
let option = options[i];
|
||||||
|
if (option.selected) {
|
||||||
|
let next = element.options[i + 1];
|
||||||
|
option = element.removeChild(option);
|
||||||
|
next = element.replaceChild(option, next);
|
||||||
|
element.insertBefore(next, option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize move up/down buttons.
|
||||||
|
*/
|
||||||
|
export function initMoveButtons(): void {
|
||||||
|
for (const button of getElements<HTMLButtonElement>('#move-option-up')) {
|
||||||
|
const target = button.getAttribute('data-target');
|
||||||
|
if (target !== null) {
|
||||||
|
for (const select of getElements<HTMLSelectElement>(`#${target}`)) {
|
||||||
|
button.addEventListener('click', () => moveOptionUp(select));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const button of getElements<HTMLButtonElement>('#move-option-down')) {
|
||||||
|
const target = button.getAttribute('data-target');
|
||||||
|
if (target !== null) {
|
||||||
|
for (const select of getElements<HTMLSelectElement>(`#${target}`)) {
|
||||||
|
button.addEventListener('click', () => moveOptionDown(select));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
netbox/project-static/src/buttons/pagination.ts
Normal file
14
netbox/project-static/src/buttons/pagination.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { getElements } from '../util';
|
||||||
|
|
||||||
|
function handlePerPageSelect(event: Event): void {
|
||||||
|
const select = event.currentTarget as HTMLSelectElement;
|
||||||
|
if (select.form !== null) {
|
||||||
|
select.form.submit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initPerPage(): void {
|
||||||
|
for (const element of getElements<HTMLSelectElement>('select.per-page')) {
|
||||||
|
element.addEventListener('change', handlePerPageSelect);
|
||||||
|
}
|
||||||
|
}
|
30
netbox/project-static/src/buttons/preferences.ts
Normal file
30
netbox/project-static/src/buttons/preferences.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { setColorMode } from '../colorMode';
|
||||||
|
import { getElement } from '../util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform actions in the UI based on the value of user profile updates.
|
||||||
|
*
|
||||||
|
* @param event Form Submit
|
||||||
|
*/
|
||||||
|
function handlePreferenceSave(event: Event): void {
|
||||||
|
// Create a FormData instance to access the form values.
|
||||||
|
const form = event.currentTarget as HTMLFormElement;
|
||||||
|
const formData = new FormData(form);
|
||||||
|
|
||||||
|
// Update the UI color mode immediately when the user preference changes.
|
||||||
|
if (formData.get('ui.colormode') === 'dark') {
|
||||||
|
setColorMode('dark');
|
||||||
|
} else if (formData.get('ui.colormode') === 'light') {
|
||||||
|
setColorMode('light');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize handlers for user profile updates.
|
||||||
|
*/
|
||||||
|
export function initPreferenceUpdate(): void {
|
||||||
|
const form = getElement<HTMLFormElement>('preferences-update');
|
||||||
|
if (form !== null) {
|
||||||
|
form.addEventListener('submit', handlePreferenceSave);
|
||||||
|
}
|
||||||
|
}
|
46
netbox/project-static/src/buttons/reslug.ts
Normal file
46
netbox/project-static/src/buttons/reslug.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* Create a slug from any input string.
|
||||||
|
*
|
||||||
|
* @param slug Original string.
|
||||||
|
* @param chars Maximum number of characters.
|
||||||
|
* @returns Slugified string.
|
||||||
|
*/
|
||||||
|
function slugify(slug: string, chars: number): string {
|
||||||
|
return slug
|
||||||
|
.replace(/[^\-.\w\s]/g, '') // Remove unneeded chars
|
||||||
|
.replace(/^[\s.]+|[\s.]+$/g, '') // Trim leading/trailing spaces
|
||||||
|
.replace(/[-.\s]+/g, '-') // Convert spaces and decimals to hyphens
|
||||||
|
.toLowerCase() // Convert to lowercase
|
||||||
|
.substring(0, chars); // Trim to first chars chars
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a slug field exists, add event listeners to handle automatically generating its value.
|
||||||
|
*/
|
||||||
|
export function initReslug(): void {
|
||||||
|
const slugField = document.getElementById('id_slug') as HTMLInputElement;
|
||||||
|
const slugButton = document.getElementById('reslug') as HTMLButtonElement;
|
||||||
|
if (slugField === null || slugButton === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sourceId = slugField.getAttribute('slug-source');
|
||||||
|
const sourceField = document.getElementById(`id_${sourceId}`) as HTMLInputElement;
|
||||||
|
|
||||||
|
if (sourceField === null) {
|
||||||
|
console.error('Unable to find field for slug field.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const slugLengthAttr = slugField.getAttribute('maxlength');
|
||||||
|
let slugLength = 50;
|
||||||
|
|
||||||
|
if (slugLengthAttr) {
|
||||||
|
slugLength = Number(slugLengthAttr);
|
||||||
|
}
|
||||||
|
sourceField.addEventListener('blur', () => {
|
||||||
|
slugField.value = slugify(sourceField.value, slugLength);
|
||||||
|
});
|
||||||
|
slugButton.addEventListener('click', () => {
|
||||||
|
slugField.value = slugify(sourceField.value, slugLength);
|
||||||
|
});
|
||||||
|
}
|
106
netbox/project-static/src/buttons/selectAll.ts
Normal file
106
netbox/project-static/src/buttons/selectAll.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { getElement, getElements, findFirstAdjacent } from '../util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If any PK checkbox is checked, uncheck the select all table checkbox and the select all
|
||||||
|
* confirmation checkbox.
|
||||||
|
*
|
||||||
|
* @param event Change Event
|
||||||
|
*/
|
||||||
|
function handlePkCheck(event: Event): void {
|
||||||
|
const target = event.currentTarget as HTMLInputElement;
|
||||||
|
if (!target.checked) {
|
||||||
|
for (const element of getElements<HTMLInputElement>(
|
||||||
|
'input[type="checkbox"].toggle',
|
||||||
|
'input#select-all',
|
||||||
|
)) {
|
||||||
|
element.checked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the select all card when the select all checkbox is checked, and sync the checkbox state
|
||||||
|
* with all the PK checkboxes in the table.
|
||||||
|
*
|
||||||
|
* @param event Change Event
|
||||||
|
*/
|
||||||
|
function handleSelectAllToggle(event: Event): void {
|
||||||
|
// Select all checkbox in header row.
|
||||||
|
const tableSelectAll = event.currentTarget as HTMLInputElement;
|
||||||
|
// Nearest table to the select all checkbox.
|
||||||
|
const table = findFirstAdjacent<HTMLInputElement>(tableSelectAll, 'table');
|
||||||
|
// Select all confirmation card.
|
||||||
|
const confirmCard = document.getElementById('select-all-box');
|
||||||
|
// Checkbox in confirmation card to signal if all objects should be selected.
|
||||||
|
const confirmCheckbox = document.getElementById('select-all') as Nullable<HTMLInputElement>;
|
||||||
|
|
||||||
|
if (table !== null) {
|
||||||
|
for (const element of table.querySelectorAll<HTMLInputElement>(
|
||||||
|
'input[type="checkbox"][name="pk"]',
|
||||||
|
)) {
|
||||||
|
if (tableSelectAll.checked) {
|
||||||
|
// Check all PK checkboxes if the select all checkbox is checked.
|
||||||
|
element.checked = true;
|
||||||
|
} else {
|
||||||
|
// Uncheck all PK checkboxes if the select all checkbox is unchecked.
|
||||||
|
element.checked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (confirmCard !== null) {
|
||||||
|
if (tableSelectAll.checked) {
|
||||||
|
// Unhide the select all confirmation card if the select all checkbox is checked.
|
||||||
|
confirmCard.classList.remove('d-none');
|
||||||
|
} else {
|
||||||
|
// Hide the select all confirmation card if the select all checkbox is unchecked.
|
||||||
|
confirmCard.classList.add('d-none');
|
||||||
|
if (confirmCheckbox !== null) {
|
||||||
|
// Uncheck the confirmation checkbox when the table checkbox is unchecked (after which
|
||||||
|
// the confirmation card will be hidden).
|
||||||
|
confirmCheckbox.checked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronize the select all confirmation checkbox state with the select all confirmation button
|
||||||
|
* disabled state. If the select all confirmation checkbox is checked, the buttons should be
|
||||||
|
* enabled. If not, the buttons should be disabled.
|
||||||
|
*
|
||||||
|
* @param event Change Event
|
||||||
|
*/
|
||||||
|
function handleSelectAll(event: Event): void {
|
||||||
|
const target = event.currentTarget as HTMLInputElement;
|
||||||
|
const selectAllBox = getElement<HTMLDivElement>('select-all-box');
|
||||||
|
if (selectAllBox !== null) {
|
||||||
|
for (const button of selectAllBox.querySelectorAll<HTMLButtonElement>(
|
||||||
|
'button[type="submit"]',
|
||||||
|
)) {
|
||||||
|
if (target.checked) {
|
||||||
|
button.disabled = false;
|
||||||
|
} else {
|
||||||
|
button.disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize table select all elements.
|
||||||
|
*/
|
||||||
|
export function initSelectAll(): void {
|
||||||
|
for (const element of getElements<HTMLInputElement>(
|
||||||
|
'table tr th > input[type="checkbox"].toggle',
|
||||||
|
)) {
|
||||||
|
element.addEventListener('change', handleSelectAllToggle);
|
||||||
|
}
|
||||||
|
for (const element of getElements<HTMLInputElement>('input[type="checkbox"][name="pk"]')) {
|
||||||
|
element.addEventListener('change', handlePkCheck);
|
||||||
|
}
|
||||||
|
const selectAll = getElement<HTMLInputElement>('select-all');
|
||||||
|
|
||||||
|
if (selectAll !== null) {
|
||||||
|
selectAll.addEventListener('change', handleSelectAll);
|
||||||
|
}
|
||||||
|
}
|
@ -1,303 +0,0 @@
|
|||||||
import { getElements, scrollTo, findFirstAdjacent, isTruthy } from './util';
|
|
||||||
|
|
||||||
type ShowHideMap = {
|
|
||||||
default: { hide: string[]; show: string[] };
|
|
||||||
[k: string]: { hide: string[]; show: string[] };
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle bulk add/edit/rename form actions.
|
|
||||||
*
|
|
||||||
* @param event Click Event
|
|
||||||
*/
|
|
||||||
function handleFormActionClick(event: Event): void {
|
|
||||||
event.preventDefault();
|
|
||||||
const element = event.currentTarget as HTMLElement;
|
|
||||||
if (element !== null) {
|
|
||||||
const form = findFirstAdjacent<HTMLFormElement>(element, 'form');
|
|
||||||
const href = element.getAttribute('href');
|
|
||||||
if (form !== null && isTruthy(href)) {
|
|
||||||
form.setAttribute('action', href);
|
|
||||||
form.submit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize bulk form action links.
|
|
||||||
*/
|
|
||||||
function initFormActions() {
|
|
||||||
for (const element of getElements<HTMLAnchorElement>('a.formaction')) {
|
|
||||||
element.addEventListener('click', handleFormActionClick);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get form data from a form element and transform it into a body usable by fetch.
|
|
||||||
*
|
|
||||||
* @param element Form element
|
|
||||||
* @returns Fetch body
|
|
||||||
*/
|
|
||||||
export function getFormData(element: HTMLFormElement): URLSearchParams {
|
|
||||||
const formData = new FormData(element);
|
|
||||||
const body = new URLSearchParams();
|
|
||||||
for (const [k, v] of formData) {
|
|
||||||
body.append(k, v as string);
|
|
||||||
}
|
|
||||||
return body;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the value of the number input field based on the selection of the dropdown.
|
|
||||||
*/
|
|
||||||
function initSpeedSelector(): void {
|
|
||||||
for (const element of getElements<HTMLAnchorElement>('a.set_speed')) {
|
|
||||||
if (element !== null) {
|
|
||||||
function handleClick(event: Event) {
|
|
||||||
// Don't reload the page (due to href="#").
|
|
||||||
event.preventDefault();
|
|
||||||
// Get the value of the `data` attribute on the dropdown option.
|
|
||||||
const value = element.getAttribute('data');
|
|
||||||
// Find the input element referenced by the dropdown element.
|
|
||||||
const input = document.getElementById(element.target) as Nullable<HTMLInputElement>;
|
|
||||||
if (input !== null && value !== null) {
|
|
||||||
// Set the value of the input field to the `data` attribute's value.
|
|
||||||
input.value = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
element.addEventListener('click', handleClick);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleFormSubmit(event: Event, form: HTMLFormElement): void {
|
|
||||||
// Track the names of each invalid field.
|
|
||||||
const invalids = new Set<string>();
|
|
||||||
|
|
||||||
for (const element of form.querySelectorAll<FormControls>('*[name]')) {
|
|
||||||
if (!element.validity.valid) {
|
|
||||||
invalids.add(element.name);
|
|
||||||
|
|
||||||
// If the field is invalid, but contains the .is-valid class, remove it.
|
|
||||||
if (element.classList.contains('is-valid')) {
|
|
||||||
element.classList.remove('is-valid');
|
|
||||||
}
|
|
||||||
// If the field is invalid, but doesn't contain the .is-invalid class, add it.
|
|
||||||
if (!element.classList.contains('is-invalid')) {
|
|
||||||
element.classList.add('is-invalid');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If the field is valid, but contains the .is-invalid class, remove it.
|
|
||||||
if (element.classList.contains('is-invalid')) {
|
|
||||||
element.classList.remove('is-invalid');
|
|
||||||
}
|
|
||||||
// If the field is valid, but doesn't contain the .is-valid class, add it.
|
|
||||||
if (!element.classList.contains('is-valid')) {
|
|
||||||
element.classList.add('is-valid');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (invalids.size !== 0) {
|
|
||||||
// If there are invalid fields, pick the first field and scroll to it.
|
|
||||||
const firstInvalid = form.elements.namedItem(Array.from(invalids)[0]) as Element;
|
|
||||||
scrollTo(firstInvalid);
|
|
||||||
|
|
||||||
// If the form has invalid fields, don't submit it.
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attach an event listener to each form's submitter (button[type=submit]). When called, the
|
|
||||||
* callback checks the validity of each form field and adds the appropriate Bootstrap CSS class
|
|
||||||
* based on the field's validity.
|
|
||||||
*/
|
|
||||||
function initFormElements() {
|
|
||||||
for (const form of getElements('form')) {
|
|
||||||
// Find each of the form's submitters. Most object edit forms have a "Create" and
|
|
||||||
// a "Create & Add", so we need to add a listener to both.
|
|
||||||
const submitters = form.querySelectorAll<HTMLButtonElement>('button[type=submit]');
|
|
||||||
|
|
||||||
for (const submitter of submitters) {
|
|
||||||
// Add the event listener to each submitter.
|
|
||||||
submitter.addEventListener('click', event => handleFormSubmit(event, form));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move selected options of a select element up in order.
|
|
||||||
*
|
|
||||||
* Adapted from:
|
|
||||||
* @see https://www.tomred.net/css-html-js/reorder-option-elements-of-an-html-select.html
|
|
||||||
* @param element Select Element
|
|
||||||
*/
|
|
||||||
function moveOptionUp(element: HTMLSelectElement): void {
|
|
||||||
const options = Array.from(element.options);
|
|
||||||
for (let i = 1; i < options.length; i++) {
|
|
||||||
const option = options[i];
|
|
||||||
if (option.selected) {
|
|
||||||
element.removeChild(option);
|
|
||||||
element.insertBefore(option, element.options[i - 1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move selected options of a select element down in order.
|
|
||||||
*
|
|
||||||
* Adapted from:
|
|
||||||
* @see https://www.tomred.net/css-html-js/reorder-option-elements-of-an-html-select.html
|
|
||||||
* @param element Select Element
|
|
||||||
*/
|
|
||||||
function moveOptionDown(element: HTMLSelectElement): void {
|
|
||||||
const options = Array.from(element.options);
|
|
||||||
for (let i = options.length - 2; i >= 0; i--) {
|
|
||||||
let option = options[i];
|
|
||||||
if (option.selected) {
|
|
||||||
let next = element.options[i + 1];
|
|
||||||
option = element.removeChild(option);
|
|
||||||
next = element.replaceChild(option, next);
|
|
||||||
element.insertBefore(next, option);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize move up/down buttons.
|
|
||||||
*/
|
|
||||||
function initMoveButtons() {
|
|
||||||
for (const button of getElements<HTMLButtonElement>('#move-option-up')) {
|
|
||||||
const target = button.getAttribute('data-target');
|
|
||||||
if (target !== null) {
|
|
||||||
for (const select of getElements<HTMLSelectElement>(`#${target}`)) {
|
|
||||||
button.addEventListener('click', () => moveOptionUp(select));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const button of getElements<HTMLButtonElement>('#move-option-down')) {
|
|
||||||
const target = button.getAttribute('data-target');
|
|
||||||
if (target !== null) {
|
|
||||||
for (const select of getElements<HTMLSelectElement>(`#${target}`)) {
|
|
||||||
button.addEventListener('click', () => moveOptionDown(select));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mapping of scope names to arrays of object types whose fields should be hidden or shown when
|
|
||||||
* the scope type (key) is selected.
|
|
||||||
*
|
|
||||||
* For example, if `region` is the scope type, the fields with IDs listed in
|
|
||||||
* showHideMap.region.hide should be hidden, and the fields with IDs listed in
|
|
||||||
* showHideMap.region.show should be shown.
|
|
||||||
*/
|
|
||||||
const showHideMap: ShowHideMap = {
|
|
||||||
region: {
|
|
||||||
hide: ['id_sitegroup', 'id_site', 'id_location', 'id_rack', 'id_clustergroup', 'id_cluster'],
|
|
||||||
show: ['id_region'],
|
|
||||||
},
|
|
||||||
'site group': {
|
|
||||||
hide: ['id_region', 'id_site', 'id_location', 'id_rack', 'id_clustergroup', 'id_cluster'],
|
|
||||||
show: ['id_sitegroup'],
|
|
||||||
},
|
|
||||||
site: {
|
|
||||||
hide: ['id_location', 'id_rack', 'id_clustergroup', 'id_cluster'],
|
|
||||||
show: ['id_region', 'id_sitegroup', 'id_site'],
|
|
||||||
},
|
|
||||||
location: {
|
|
||||||
hide: ['id_rack', 'id_clustergroup', 'id_cluster'],
|
|
||||||
show: ['id_region', 'id_sitegroup', 'id_site', 'id_location'],
|
|
||||||
},
|
|
||||||
rack: {
|
|
||||||
hide: ['id_clustergroup', 'id_cluster'],
|
|
||||||
show: ['id_region', 'id_sitegroup', 'id_site', 'id_location', 'id_rack'],
|
|
||||||
},
|
|
||||||
'cluster group': {
|
|
||||||
hide: ['id_region', 'id_sitegroup', 'id_site', 'id_location', 'id_rack', 'id_cluster'],
|
|
||||||
show: ['id_clustergroup'],
|
|
||||||
},
|
|
||||||
cluster: {
|
|
||||||
hide: ['id_region', 'id_sitegroup', 'id_site', 'id_location', 'id_rack'],
|
|
||||||
show: ['id_clustergroup', 'id_cluster'],
|
|
||||||
},
|
|
||||||
default: {
|
|
||||||
hide: [
|
|
||||||
'id_region',
|
|
||||||
'id_sitegroup',
|
|
||||||
'id_site',
|
|
||||||
'id_location',
|
|
||||||
'id_rack',
|
|
||||||
'id_clustergroup',
|
|
||||||
'id_cluster',
|
|
||||||
],
|
|
||||||
show: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle visibility of a given element's parent.
|
|
||||||
* @param query CSS Query.
|
|
||||||
* @param action Show or Hide the Parent.
|
|
||||||
*/
|
|
||||||
function toggleParentVisibility(query: string, action: 'show' | 'hide') {
|
|
||||||
for (const element of getElements(query)) {
|
|
||||||
if (action === 'show') {
|
|
||||||
element.parentElement?.classList.remove('d-none', 'invisible');
|
|
||||||
} else {
|
|
||||||
element.parentElement?.classList.add('d-none', 'invisible');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle changes to the Scope Type field.
|
|
||||||
*/
|
|
||||||
function handleScopeChange(event: Event) {
|
|
||||||
const element = event.currentTarget as HTMLSelectElement;
|
|
||||||
// Scope type's innerText looks something like `DCIM > region`.
|
|
||||||
const scopeType = element.options[element.selectedIndex].innerText.toLowerCase();
|
|
||||||
|
|
||||||
for (const [scope, fields] of Object.entries(showHideMap)) {
|
|
||||||
// If the scope type ends with the specified scope, toggle its field visibility according to
|
|
||||||
// the show/hide values.
|
|
||||||
if (scopeType.endsWith(scope)) {
|
|
||||||
for (const field of fields.hide) {
|
|
||||||
toggleParentVisibility(`#${field}`, 'hide');
|
|
||||||
}
|
|
||||||
for (const field of fields.show) {
|
|
||||||
toggleParentVisibility(`#${field}`, 'show');
|
|
||||||
}
|
|
||||||
// Stop on first match.
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
// Otherwise, hide all fields.
|
|
||||||
for (const field of showHideMap.default.hide) {
|
|
||||||
toggleParentVisibility(`#${field}`, 'hide');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize scope type select event listeners.
|
|
||||||
*/
|
|
||||||
function initScopeSelector() {
|
|
||||||
for (const element of getElements<HTMLSelectElement>('#id_scope_type')) {
|
|
||||||
element.addEventListener('change', handleScopeChange);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initForms(): void {
|
|
||||||
for (const func of [
|
|
||||||
initFormElements,
|
|
||||||
initFormActions,
|
|
||||||
initMoveButtons,
|
|
||||||
initSpeedSelector,
|
|
||||||
initScopeSelector,
|
|
||||||
]) {
|
|
||||||
func();
|
|
||||||
}
|
|
||||||
}
|
|
28
netbox/project-static/src/forms/actions.ts
Normal file
28
netbox/project-static/src/forms/actions.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { getElements, findFirstAdjacent, isTruthy } from '../util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle bulk add/edit/rename form actions.
|
||||||
|
*
|
||||||
|
* @param event Click Event
|
||||||
|
*/
|
||||||
|
function handleFormActionClick(event: Event): void {
|
||||||
|
event.preventDefault();
|
||||||
|
const element = event.currentTarget as HTMLElement;
|
||||||
|
if (element !== null) {
|
||||||
|
const form = findFirstAdjacent<HTMLFormElement>(element, 'form');
|
||||||
|
const href = element.getAttribute('href');
|
||||||
|
if (form !== null && isTruthy(href)) {
|
||||||
|
form.setAttribute('action', href);
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize bulk form action links.
|
||||||
|
*/
|
||||||
|
export function initFormActions(): void {
|
||||||
|
for (const element of getElements<HTMLAnchorElement>('a.formaction')) {
|
||||||
|
element.addEventListener('click', handleFormActionClick);
|
||||||
|
}
|
||||||
|
}
|
57
netbox/project-static/src/forms/elements.ts
Normal file
57
netbox/project-static/src/forms/elements.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { getElements, scrollTo } from '../util';
|
||||||
|
|
||||||
|
function handleFormSubmit(event: Event, form: HTMLFormElement): void {
|
||||||
|
// Track the names of each invalid field.
|
||||||
|
const invalids = new Set<string>();
|
||||||
|
|
||||||
|
for (const element of form.querySelectorAll<FormControls>('*[name]')) {
|
||||||
|
if (!element.validity.valid) {
|
||||||
|
invalids.add(element.name);
|
||||||
|
|
||||||
|
// If the field is invalid, but contains the .is-valid class, remove it.
|
||||||
|
if (element.classList.contains('is-valid')) {
|
||||||
|
element.classList.remove('is-valid');
|
||||||
|
}
|
||||||
|
// If the field is invalid, but doesn't contain the .is-invalid class, add it.
|
||||||
|
if (!element.classList.contains('is-invalid')) {
|
||||||
|
element.classList.add('is-invalid');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If the field is valid, but contains the .is-invalid class, remove it.
|
||||||
|
if (element.classList.contains('is-invalid')) {
|
||||||
|
element.classList.remove('is-invalid');
|
||||||
|
}
|
||||||
|
// If the field is valid, but doesn't contain the .is-valid class, add it.
|
||||||
|
if (!element.classList.contains('is-valid')) {
|
||||||
|
element.classList.add('is-valid');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invalids.size !== 0) {
|
||||||
|
// If there are invalid fields, pick the first field and scroll to it.
|
||||||
|
const firstInvalid = form.elements.namedItem(Array.from(invalids)[0]) as Element;
|
||||||
|
scrollTo(firstInvalid);
|
||||||
|
|
||||||
|
// If the form has invalid fields, don't submit it.
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach an event listener to each form's submitter (button[type=submit]). When called, the
|
||||||
|
* callback checks the validity of each form field and adds the appropriate Bootstrap CSS class
|
||||||
|
* based on the field's validity.
|
||||||
|
*/
|
||||||
|
export function initFormElements(): void {
|
||||||
|
for (const form of getElements('form')) {
|
||||||
|
// Find each of the form's submitters. Most object edit forms have a "Create" and
|
||||||
|
// a "Create & Add", so we need to add a listener to both.
|
||||||
|
const submitters = form.querySelectorAll<HTMLButtonElement>('button[type=submit]');
|
||||||
|
|
||||||
|
for (const submitter of submitters) {
|
||||||
|
// Add the event listener to each submitter.
|
||||||
|
submitter.addEventListener('click', (event: Event) => handleFormSubmit(event, form));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
netbox/project-static/src/forms/index.ts
Normal file
17
netbox/project-static/src/forms/index.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { initFormActions } from './actions';
|
||||||
|
import { initFormElements } from './elements';
|
||||||
|
import { initSpeedSelector } from './speedSelector';
|
||||||
|
import { initScopeSelector } from './scopeSelector';
|
||||||
|
import { initVlanTags } from './vlanTags';
|
||||||
|
|
||||||
|
export function initForms(): void {
|
||||||
|
for (const func of [
|
||||||
|
initFormActions,
|
||||||
|
initFormElements,
|
||||||
|
initSpeedSelector,
|
||||||
|
initScopeSelector,
|
||||||
|
initVlanTags,
|
||||||
|
]) {
|
||||||
|
func();
|
||||||
|
}
|
||||||
|
}
|
109
netbox/project-static/src/forms/scopeSelector.ts
Normal file
109
netbox/project-static/src/forms/scopeSelector.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import { getElements } from '../util';
|
||||||
|
|
||||||
|
type ShowHideMap = {
|
||||||
|
default: { hide: string[]; show: string[] };
|
||||||
|
[k: string]: { hide: string[]; show: string[] };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping of scope names to arrays of object types whose fields should be hidden or shown when
|
||||||
|
* the scope type (key) is selected.
|
||||||
|
*
|
||||||
|
* For example, if `region` is the scope type, the fields with IDs listed in
|
||||||
|
* showHideMap.region.hide should be hidden, and the fields with IDs listed in
|
||||||
|
* showHideMap.region.show should be shown.
|
||||||
|
*/
|
||||||
|
const showHideMap: ShowHideMap = {
|
||||||
|
region: {
|
||||||
|
hide: ['id_sitegroup', 'id_site', 'id_location', 'id_rack', 'id_clustergroup', 'id_cluster'],
|
||||||
|
show: ['id_region'],
|
||||||
|
},
|
||||||
|
'site group': {
|
||||||
|
hide: ['id_region', 'id_site', 'id_location', 'id_rack', 'id_clustergroup', 'id_cluster'],
|
||||||
|
show: ['id_sitegroup'],
|
||||||
|
},
|
||||||
|
site: {
|
||||||
|
hide: ['id_location', 'id_rack', 'id_clustergroup', 'id_cluster'],
|
||||||
|
show: ['id_region', 'id_sitegroup', 'id_site'],
|
||||||
|
},
|
||||||
|
location: {
|
||||||
|
hide: ['id_rack', 'id_clustergroup', 'id_cluster'],
|
||||||
|
show: ['id_region', 'id_sitegroup', 'id_site', 'id_location'],
|
||||||
|
},
|
||||||
|
rack: {
|
||||||
|
hide: ['id_clustergroup', 'id_cluster'],
|
||||||
|
show: ['id_region', 'id_sitegroup', 'id_site', 'id_location', 'id_rack'],
|
||||||
|
},
|
||||||
|
'cluster group': {
|
||||||
|
hide: ['id_region', 'id_sitegroup', 'id_site', 'id_location', 'id_rack', 'id_cluster'],
|
||||||
|
show: ['id_clustergroup'],
|
||||||
|
},
|
||||||
|
cluster: {
|
||||||
|
hide: ['id_region', 'id_sitegroup', 'id_site', 'id_location', 'id_rack'],
|
||||||
|
show: ['id_clustergroup', 'id_cluster'],
|
||||||
|
},
|
||||||
|
default: {
|
||||||
|
hide: [
|
||||||
|
'id_region',
|
||||||
|
'id_sitegroup',
|
||||||
|
'id_site',
|
||||||
|
'id_location',
|
||||||
|
'id_rack',
|
||||||
|
'id_clustergroup',
|
||||||
|
'id_cluster',
|
||||||
|
],
|
||||||
|
show: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Toggle visibility of a given element's parent.
|
||||||
|
* @param query CSS Query.
|
||||||
|
* @param action Show or Hide the Parent.
|
||||||
|
*/
|
||||||
|
function toggleParentVisibility(query: string, action: 'show' | 'hide') {
|
||||||
|
for (const element of getElements(query)) {
|
||||||
|
if (action === 'show') {
|
||||||
|
element.parentElement?.classList.remove('d-none', 'invisible');
|
||||||
|
} else {
|
||||||
|
element.parentElement?.classList.add('d-none', 'invisible');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle changes to the Scope Type field.
|
||||||
|
*/
|
||||||
|
function handleScopeChange(event: Event) {
|
||||||
|
const element = event.currentTarget as HTMLSelectElement;
|
||||||
|
// Scope type's innerText looks something like `DCIM > region`.
|
||||||
|
const scopeType = element.options[element.selectedIndex].innerText.toLowerCase();
|
||||||
|
|
||||||
|
for (const [scope, fields] of Object.entries(showHideMap)) {
|
||||||
|
// If the scope type ends with the specified scope, toggle its field visibility according to
|
||||||
|
// the show/hide values.
|
||||||
|
if (scopeType.endsWith(scope)) {
|
||||||
|
for (const field of fields.hide) {
|
||||||
|
toggleParentVisibility(`#${field}`, 'hide');
|
||||||
|
}
|
||||||
|
for (const field of fields.show) {
|
||||||
|
toggleParentVisibility(`#${field}`, 'show');
|
||||||
|
}
|
||||||
|
// Stop on first match.
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
// Otherwise, hide all fields.
|
||||||
|
for (const field of showHideMap.default.hide) {
|
||||||
|
toggleParentVisibility(`#${field}`, 'hide');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize scope type select event listeners.
|
||||||
|
*/
|
||||||
|
export function initScopeSelector(): void {
|
||||||
|
for (const element of getElements<HTMLSelectElement>('#id_scope_type')) {
|
||||||
|
element.addEventListener('change', handleScopeChange);
|
||||||
|
}
|
||||||
|
}
|
24
netbox/project-static/src/forms/speedSelector.ts
Normal file
24
netbox/project-static/src/forms/speedSelector.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { getElements } from '../util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the value of the number input field based on the selection of the dropdown.
|
||||||
|
*/
|
||||||
|
export function initSpeedSelector(): void {
|
||||||
|
for (const element of getElements<HTMLAnchorElement>('a.set_speed')) {
|
||||||
|
if (element !== null) {
|
||||||
|
function handleClick(event: Event) {
|
||||||
|
// Don't reload the page (due to href="#").
|
||||||
|
event.preventDefault();
|
||||||
|
// Get the value of the `data` attribute on the dropdown option.
|
||||||
|
const value = element.getAttribute('data');
|
||||||
|
// Find the input element referenced by the dropdown element.
|
||||||
|
const input = document.getElementById(element.target) as Nullable<HTMLInputElement>;
|
||||||
|
if (input !== null && value !== null) {
|
||||||
|
// Set the value of the input field to the `data` attribute's value.
|
||||||
|
input.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
element.addEventListener('click', handleClick);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
116
netbox/project-static/src/forms/vlanTags.ts
Normal file
116
netbox/project-static/src/forms/vlanTags.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { all, getElement, resetSelect, toggleVisibility } from '../util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a select element's containing `.row` element.
|
||||||
|
*
|
||||||
|
* @param element Select element.
|
||||||
|
* @returns Containing row element.
|
||||||
|
*/
|
||||||
|
function fieldContainer(element: Nullable<HTMLSelectElement>): Nullable<HTMLElement> {
|
||||||
|
const container = element?.parentElement?.parentElement ?? null;
|
||||||
|
if (container !== null && container.classList.contains('row')) {
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle element visibility when the mode field does not have a value.
|
||||||
|
*/
|
||||||
|
function handleModeNone(): void {
|
||||||
|
const elements = [
|
||||||
|
getElement<HTMLSelectElement>('id_tagged_vlans'),
|
||||||
|
getElement<HTMLSelectElement>('id_untagged_vlan'),
|
||||||
|
getElement<HTMLSelectElement>('id_vlan_group'),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (all(elements)) {
|
||||||
|
const [taggedVlans, untaggedVlan] = elements;
|
||||||
|
resetSelect(untaggedVlan);
|
||||||
|
resetSelect(taggedVlans);
|
||||||
|
for (const element of elements) {
|
||||||
|
toggleVisibility(fieldContainer(element), 'hide');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle element visibility when the mode field's value is Access.
|
||||||
|
*/
|
||||||
|
function handleModeAccess(): void {
|
||||||
|
const elements = [
|
||||||
|
getElement<HTMLSelectElement>('id_tagged_vlans'),
|
||||||
|
getElement<HTMLSelectElement>('id_untagged_vlan'),
|
||||||
|
getElement<HTMLSelectElement>('id_vlan_group'),
|
||||||
|
];
|
||||||
|
if (all(elements)) {
|
||||||
|
const [taggedVlans, untaggedVlan, vlanGroup] = elements;
|
||||||
|
resetSelect(taggedVlans);
|
||||||
|
toggleVisibility(fieldContainer(vlanGroup), 'show');
|
||||||
|
toggleVisibility(fieldContainer(untaggedVlan), 'show');
|
||||||
|
toggleVisibility(fieldContainer(taggedVlans), 'hide');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle element visibility when the mode field's value is Tagged.
|
||||||
|
*/
|
||||||
|
function handleModeTagged(): void {
|
||||||
|
const elements = [
|
||||||
|
getElement<HTMLSelectElement>('id_tagged_vlans'),
|
||||||
|
getElement<HTMLSelectElement>('id_untagged_vlan'),
|
||||||
|
getElement<HTMLSelectElement>('id_vlan_group'),
|
||||||
|
];
|
||||||
|
if (all(elements)) {
|
||||||
|
const [taggedVlans, untaggedVlan, vlanGroup] = elements;
|
||||||
|
toggleVisibility(fieldContainer(taggedVlans), 'show');
|
||||||
|
toggleVisibility(fieldContainer(vlanGroup), 'show');
|
||||||
|
toggleVisibility(fieldContainer(untaggedVlan), 'show');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle element visibility when the mode field's value is Tagged (All).
|
||||||
|
*/
|
||||||
|
function handleModeTaggedAll(): void {
|
||||||
|
const elements = [
|
||||||
|
getElement<HTMLSelectElement>('id_tagged_vlans'),
|
||||||
|
getElement<HTMLSelectElement>('id_untagged_vlan'),
|
||||||
|
getElement<HTMLSelectElement>('id_vlan_group'),
|
||||||
|
];
|
||||||
|
if (all(elements)) {
|
||||||
|
const [taggedVlans, untaggedVlan, vlanGroup] = elements;
|
||||||
|
resetSelect(taggedVlans);
|
||||||
|
toggleVisibility(fieldContainer(vlanGroup), 'show');
|
||||||
|
toggleVisibility(fieldContainer(untaggedVlan), 'show');
|
||||||
|
toggleVisibility(fieldContainer(taggedVlans), 'hide');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset field visibility when the mode field's value changes.
|
||||||
|
*/
|
||||||
|
function handleModeChange(element: HTMLSelectElement): void {
|
||||||
|
switch (element.value) {
|
||||||
|
case 'access':
|
||||||
|
handleModeAccess();
|
||||||
|
break;
|
||||||
|
case 'tagged':
|
||||||
|
handleModeTagged();
|
||||||
|
break;
|
||||||
|
case 'tagged-all':
|
||||||
|
handleModeTaggedAll();
|
||||||
|
break;
|
||||||
|
case '':
|
||||||
|
handleModeNone();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initVlanTags(): void {
|
||||||
|
const element = getElement<HTMLSelectElement>('id_mode');
|
||||||
|
if (element !== null) {
|
||||||
|
element.addEventListener('change', () => handleModeChange(element));
|
||||||
|
handleModeChange(element);
|
||||||
|
}
|
||||||
|
}
|
@ -26,22 +26,6 @@ export function hasMore(data: APIAnswer<APIObjectBase>): data is APIAnswerWithNe
|
|||||||
return typeof data.next === 'string';
|
return typeof data.next === 'string';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a slug from any input string.
|
|
||||||
*
|
|
||||||
* @param slug Original string.
|
|
||||||
* @param chars Maximum number of characters.
|
|
||||||
* @returns Slugified string.
|
|
||||||
*/
|
|
||||||
export function slugify(slug: string, chars: number): string {
|
|
||||||
return slug
|
|
||||||
.replace(/[^\-.\w\s]/g, '') // Remove unneeded chars
|
|
||||||
.replace(/^[\s.]+|[\s.]+$/g, '') // Trim leading/trailing spaces
|
|
||||||
.replace(/[-.\s]+/g, '-') // Convert spaces and decimals to hyphens
|
|
||||||
.toLowerCase() // Convert to lowercase
|
|
||||||
.substring(0, chars); // Trim to first chars chars
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type guard to determine if a value is not null, undefined, or empty.
|
* Type guard to determine if a value is not null, undefined, or empty.
|
||||||
*/
|
*/
|
||||||
@ -59,6 +43,45 @@ export function isTruthy<V extends unknown>(value: V): value is NonNullable<V> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard to determine if all elements of an array are not null or undefined.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```js
|
||||||
|
* const elements = [document.getElementById("element1"), document.getElementById("element2")];
|
||||||
|
* if (all(elements)) {
|
||||||
|
* const [element1, element2] = elements;
|
||||||
|
* // element1 and element2 are now of type HTMLElement, not Nullable<HTMLElement>.
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function all<T extends unknown>(values: T[]): values is NonNullable<T>[] {
|
||||||
|
return values.every(value => typeof value !== 'undefined' && value !== null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deselect all selected options and reset the field value of a select element.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```js
|
||||||
|
* const select = document.querySelectorAll<HTMLSelectElement>("select.example");
|
||||||
|
* select.value = "test";
|
||||||
|
* console.log(select.value);
|
||||||
|
* // test
|
||||||
|
* resetSelect(select);
|
||||||
|
* console.log(select.value);
|
||||||
|
* // ''
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function resetSelect<S extends HTMLSelectElement>(select: S): void {
|
||||||
|
for (const option of select.options) {
|
||||||
|
if (option.selected) {
|
||||||
|
option.selected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
select.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type guard to determine if a value is an `Element`.
|
* Type guard to determine if a value is an `Element`.
|
||||||
*/
|
*/
|
||||||
@ -245,16 +268,38 @@ export function getNetboxData(key: string): string | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle visibility of an element.
|
||||||
|
*/
|
||||||
|
export function toggleVisibility<E extends HTMLElement | SVGElement>(
|
||||||
|
element: E | null,
|
||||||
|
action?: 'show' | 'hide',
|
||||||
|
): void {
|
||||||
|
if (element !== null) {
|
||||||
|
if (typeof action === 'undefined') {
|
||||||
|
// No action is passed, so we should toggle the existing state.
|
||||||
|
const current = window.getComputedStyle(element).display;
|
||||||
|
if (current === 'none') {
|
||||||
|
element.style.display = '';
|
||||||
|
} else {
|
||||||
|
element.style.display = 'none';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (action === 'show') {
|
||||||
|
element.style.display = '';
|
||||||
|
} else {
|
||||||
|
element.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle visibility of card loader.
|
* Toggle visibility of card loader.
|
||||||
*/
|
*/
|
||||||
export function toggleLoader(action: 'show' | 'hide'): void {
|
export function toggleLoader(action: 'show' | 'hide'): void {
|
||||||
for (const element of getElements<HTMLDivElement>('div.card-overlay')) {
|
for (const element of getElements<HTMLDivElement>('div.card-overlay')) {
|
||||||
if (action === 'show') {
|
toggleVisibility(element, action);
|
||||||
element.classList.remove('d-none');
|
|
||||||
} else {
|
|
||||||
element.classList.add('d-none');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user