Initial commit.

This commit is contained in:
2025-06-21 17:51:07 +01:00
commit 6fd3a23aa7
296 changed files with 29154 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
var _loading = true;
function hookupPageAccessibilityStatement() {
_loading = false;
}

59
static/js/api.js Normal file
View File

@@ -0,0 +1,59 @@
import DOM from './dom.js';
export default class API {
static getCsrfToken() {
return document.querySelector(idCSRFToken).getAttribute('content');
}
static async request(hashEndpoint, method = 'GET', data = null, params = null) {
const url = API.getUrlFromHash(hashEndpoint, params);
const csrfToken = API.getCsrfToken();
const options = {
method,
headers: {
'Content-Type': 'application/json',
[flagCsrfToken]: csrfToken,
}
};
if (data && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
data = {
...data,
[flagCsrfToken]: csrfToken,
};
options.body = JSON.stringify(data);
}
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
static getUrlFromHash(hash, params = null) {
if (hash == null) hash = hashPageHome;
let url = API.parameteriseUrl(_pathHost + hash, params);
return url;
}
static parameteriseUrl(url, params) {
if (params) {
url += '?' + new URLSearchParams(params).toString();
}
return url;
}
static goToUrl(url) {
window.location.href = url;
}
static goToHash(hash, params = null) {
const url = API.getUrlFromHash(hash, params);
API.goToUrl(url);
}
}

52
static/js/app.js Normal file
View File

@@ -0,0 +1,52 @@
'use strict';
import DOM from './dom.js';
import Router from './router.js';
class App {
constructor() {
this.dom = new DOM();
this.router = new Router();
}
initialize() {
this.setupEventListeners();
this.start();
}
setupEventListeners() {
// document.addEventListener('click', this.handleGlobalClick.bind(this));
}
handleGlobalClick(event) {
}
start() {
this.initPageCurrent();
}
initPageCurrent() {
this.router.loadPageCurrent();
}
}
const app = new App();
function domReady(fn) {
if (document.readyState !== 'loading') {
fn();
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}
domReady(() => {
app.initialize();
});
window.app = app;
export default app;

View File

@@ -0,0 +1,183 @@
import Validation from "../../../lib/validation.js";
// Date picker inputs
/*
function hookupInputDatePickers(dateInputs, notFuture, notPast, parent, addClearOption) {
if (!Validation.isEmpty(dateInputs)) {
let currentInput, currentDateString, currentDate, exceptionsArray;
for (let i = 0; i < dateInputs.length; i++) {
currentInput = document.querySelectorAll(dateInputs[i]);
currentDateString = currentInput.val();
currentDate = (!Validation.isEmpty(currentDateString)) ? convertDDMMYYYYString2Date(currentDateString, false) : null;
exceptionsArray = (currentDate != null) ? [currentDate] : null;
turnInputIntoDatePicker(currentInput, notFuture, notPast, exceptionsArray);
}
if (!Validation.isEmpty(parent)) {
// stop user from manually typing date except backspace and delete
// which will clear the whole value to ensure we either have a whole
// date string or none
parent.addEventListener("keydown", isDatePickerSelector, function(event) {
if (event.keyCode == 46 | event.keyCode == 8) { // delete or backspace
this.val('');
}
else {
event.preventDefault();
event.stopPropagation();
}
return false
});
if (addClearOption) {
// if user right-clicks in date input, give option to clear the date
parent.contextMenu({
selector: isDatePickerSelector,
delay: 100,
autoHide: true,
position: function(opt, x, y) {
var event = opt.$trigger[0]?.ownerDocument?.defaultView?.event || event;
opt.$menu.position({ my: 'center top', at: 'center top', of: event });
},
items: {
"clears": {
name: "Clear Date",
icon: "delete",
disabled: function(key, opt) { return Validation.isEmpty(document.querySelectorAll(opt.$trigger)); }, // if it's already empty, don't do anything
callback: function(itemKey, opt, rootMenu, originalEvent) { var input = document.querySelectorAll(opt.$trigger); input.val(''); input.trigger('change'); }
}
}
});
}
}
}
}
function turnInputIntoDatePicker(input, notFuture, notPast, exceptionValueArray) {
var beforeShowDayCallBack = null;
if (notFuture || notPast) {
var today = new Date();
today.setHours(0, 0, 0, 0);
var tomorrow = new Date();
tomorrow.setDate(today.getDate() + 1);
tomorrow.setHours(0, 0, 0, 0);
var hasExceptions = !Validation.isEmpty(exceptionValueArray);
beforeShowDayCallBack = function(date) {
var selectedDate = date.getTime();
var fieldHasException = hasExceptions && Validation.arrayContainsItem(exceptionValueArray, date);
if (notFuture && (tomorrow < selectedDate) && fieldHasException) return [false, 'redday', 'You cannot choose a future date'];
if (notPast && (selectedDate < today) && fieldHasException) return [false, 'redday', 'You cannot choose a past date'];
return [true, '', ''];
};
}
input.datepicker({
dateFormat: 'dd-mm-yy',
navigationAsDateFormat: true,
beforeShowDay: beforeShowDayCallBack
});
// prevent datepicker from appearing on right click
input.addEventListener('contextmenu', function() { this.datepicker('hide'); });
// Disable autocomplete suggestions appearing when clicking on input
input.getAttribute('autocomplete', 'off');
}
function setDatePickerDate(input, objDate) {
if (!Validation.isEmpty(objDate)) {
input.val('');
}
else {
input.datepicker('setDate', objDate);
}
}
function getDatePickerDate(input, adjust4DayLightSavings) {
var date = null;
if (!Validation.isEmpty(input)) {
date = input.datepicker('getDate');
if (adjust4DayLightSavings) {
formatDateDayLightSavingsTime(date);
}
}
return date;
}
function formatDateDayLightSavingsTime(date) {
// JSON.stringify removes hour delta for daylight savings
// e.g. 13/11/2023 01:00:00 goes to 13/11/2023 00:00:00
// this adds an hour so it becomes the correct time when stringified
if (!Validation.isEmpty(date)) {
date.setTime(date.getTime() - date.getTimezoneOffset() * 60 * 1000)
}
}
*/
function convertJSONDateString2Date(dateStr) {
if (Validation.isEmpty(dateStr)) return null;
if (dateStr instanceof Date) return dateStr;
return new Date(parseInt(dateStr.substr(6)));
}
function convertDDMMYYYYString2Date(dateStr, adjust4DayLightSavings) {
var date = null;
if (!Validation.isEmpty(dateStr)) {
if (dateStr instanceof Date) {
date = dateStr;
}
else {
var dateParts = dateStr.split('-');
if (dateParts.length == 3) {
date = new Date(dateParts[2], dateParts[1] - 1, dateParts[0]);
}
}
if (adjust4DayLightSavings && !Validation.isEmpty(date)) {
formatDateDayLightSavingsTime(date);
}
}
return date;
}
function convertDate2DDMMYYYYString(date) {
if (Validation.isEmpty(date)) return '';
try {
var dd = date.getDate();
var mm = date.getMonth() + 1;
var yyyy = date.getFullYear();
if (dd < 10) dd = '0' + dd;
if (dd < 10) mm = '0' + mm;
return dd + '-' + mm + '-' + yyyy;
}
catch (err) {
return 'Formatting error';
}
}

View File

@@ -0,0 +1,14 @@
function handleSelectCollapse(elementSelect) {
let optionSelected = document.querySelectorAll(elementSelect).querySelector('option:selected');
optionSelected.text(optionSelected.getAttribute(attrTextCollapsed));
optionSelected.classList.remove(flagExpanded);
optionSelected.classList.add(flagCollapsed);
}
function handleSelectExpand(elementSelect) {
let optionSelected = document.querySelectorAll(elementSelect).querySelector('option:selected');
optionSelected.text(optionSelected.getAttribute(attrTextExpanded));
optionSelected.classList.remove(flagCollapsed);
optionSelected.classList.add(flagExpanded);
}

View File

@@ -0,0 +1,45 @@
import Common from "../../../lib/common.js";
import Validation from "../../../lib/validation.js";
export default class TextArea {
removeBlankTextAreaLines(textarea) {
textarea.val(textarea.val.replace(/(?:(?:\r\n|\r|\n)\s*){2}/gm, ''));
}
fitTextAreasToContent(parent) {
var textareas = parent.querySelector('textarea');
if (!Validation.isEmpty(textareas)) {
for (var t = 0; t < textareas.length; t++) {
fitTextAreaToContent(document.querySelectorAll(textareas[t]));
}
}
}
fitTextAreaToContent(textarea) {
// Trim new text
var txtNew = textarea.val().trim();
textarea.val(txtNew);
var elTextarea = textarea[0];
// Clear style height and set rows = 1
elTextarea.style.removeProperty('height');
textarea.getAttribute('rows', 1);
const paddingTop = Common.parseFloatWithDefault(textarea.style.paddingTop);
const paddingBottom = Common.parseFloatWithDefault(textarea.style.paddingBottom);
const borderTop = Common.parseFloatWithDefault(textarea.style.borderTop);
const borderBottom = Common.parseFloatWithDefault(textarea.style.borderBottom);
let heightDelta = paddingTop + paddingBottom + borderTop + borderBottom;
let heightNew = elTextarea.scrollHeight + heightDelta;
// If new height is less than 1 linem default to single line height
const heightSingleLine = Common.parseFloatWithDefault(textarea.style.heightSingleLine) + heightDelta;
if (heightNew < heightSingleLine) heightNew = heightSingleLine;
elTextarea.style.height = heightNew + 'px';
}
}

View File

@@ -0,0 +1,21 @@
import Validation from "../../lib/validation.js";
export default class Table {
getDataTableCellByNode(table, elRow, indexColumn) {
// normal jQuery selector won't pick up hidden columns
return document.querySelectorAll(table.DataTable().cells(elRow, indexColumn, null).nodes());
}
outputTableElementDateInput(table, elRow, indexColumn, value) {
let currentCell = getDataTableCellByNode(table, elRow, indexColumn);
let dateTxt = '';
if (!Validation.isEmpty(value)) {
if (typeof value === 'string') value = convertJSONDateString2Date(value);
}
}
}

View File

@@ -0,0 +1,26 @@
import Events from "../../../lib/events.js";
export default class OverlayConfirm {
static hookup(callbackSuccess) {
Events.initialiseEventHandler(idOverlayConfirm + ' button.' + flagCancel, flagInitialised, (buttonCancel) => {
buttonCancel.addEventListener('click', () => {
let overlay = document.querySelector(idOverlayConfirm);
overlay.style.visibility = 'hidden';
});
});
Events.initialiseEventHandler(idOverlayConfirm + ' button.' + flagSubmit, flagInitialised, (buttonConfirm) => {
buttonConfirm.addEventListener('click', () => {
let overlay = document.querySelector(idOverlayConfirm);
let textarea = overlay.querySelector('textarea');
overlay.style.visibility = 'hidden';
callbackSuccess(textarea.value);
});
});
}
static show() {
let overlay = document.querySelector(idOverlayConfirm);
overlay.classList.remove(flagCollapsed);
overlay.style.visibility = 'visible';
}
}

View File

@@ -0,0 +1,19 @@
import Events from "../../../lib/events.js";
export default class OverlayError {
static hookup() {
Events.initialiseEventHandler(idOverlayError + ' button.' + flagCancel, flagInitialised, (buttonCancel) => {
buttonCancel.addEventListener('click', () => {
let overlay = document.querySelector(idOverlayError);
overlay.style.visibility = 'hidden';
});
});
}
static show(msgError) {
let overlay = document.querySelector(idOverlayError);
let labelError = overlay.querySelector(idLabelError);
labelError.innerText = msgError;
overlay.style.visibility = 'visible';
}
}

View File

@@ -0,0 +1,15 @@
import Utils from '../../lib/utils.js';
function videoPlay(elemVideo) {
if (!_loading) { // elemVideo.paused &&
elemVideo.play();
Utils.consoleLogIfNotProductionEnvironment("Playing video element: " + elemVideo.name);
}
}
function videoPause(elemVideo) {
elemVideo.pause();
Utils.consoleLogIfNotProductionEnvironment("Pausing video element: " + elemVideo.name);
}

216
static/js/dom.js Normal file
View File

@@ -0,0 +1,216 @@
import Utils from "./lib/utils.js";
import Validation from "./lib/validation.js";
export default class DOM {
static setElementAttributesValuesCurrentAndPrevious(element, data) {
DOM.setElementAttributeValueCurrent(element, data);
DOM.setElementAttributeValuePrevious(element, data);
}
static setElementAttributeValueCurrent(element, data) {
element.setAttribute(attrValueCurrent, data);
}
static setElementAttributeValuePrevious(element, data) {
element.setAttribute(attrValuePrevious, data);
}
static setElementValuesCurrentAndPrevious(element, data) {
DOM.setElementValueCurrent(element, data);
DOM.setElementAttributeValuePrevious(element, data);
}
static setElementValueCurrent(element, data) {
DOM.setElementAttributeValueCurrent(element, data);
if (element.type === "checkbox") {
element.checked = data;
}
else if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA' || element.tagName === 'SELECT') {
element.value = data;
}
else {
element.textContent = data;
}
}
static setElementValueCurrentIfEmpty(element, data) {
if (Validation.isEmpty(DOM.getElementValueCurrent(element))) {
DOM.setElementValueCurrent(element, data);
}
}
static getCellFromElement(element) {
return element.closest('td');
}
static getRowFromElement(element, flagRow) {
let selector = Validation.isEmpty(flagRow) ? 'tr' : 'tr.' + flagRow;
return element.closest(selector);
}
static getClosestParent(element, selector) {
let parent = element.parentElement;
while (parent) {
if (parent.matches(selector)) {
return parent;
}
parent = parent.parentElement;
}
return null;
}
static convertForm2JSON(elementForm) {
let dataForm = {};
if (Validation.isEmpty(elementForm)) {
return dataForm;
}
let containersFilter = elementForm.querySelectorAll('.' + flagContainerInput + '.' + flagFilter);
let containerFilter, labelFilter, keyFilter, filter;
for (let indexFilter = 0; indexFilter < containersFilter.length; indexFilter++) {
containerFilter = containersFilter[indexFilter];
labelFilter = containerFilter.querySelector('label');
keyFilter = labelFilter.getAttribute('for');
filter = containerFilter.querySelector(`#${keyFilter}`);
dataForm[keyFilter] = DOM.getElementValueCurrent(filter);
}
return dataForm;
}
static loadPageBody(contentNew) {
let pageBody = document.querySelector(idPageBody);
pageBody.innerHTML = contentNew;
}
static getHashPageCurrent() {
const hashPageCurrent = document.body.dataset.page;
return hashPageCurrent;
}
static updateAndCheckIsElementDirty(element) {
element.setAttribute(attrValueCurrent, DOM.getElementValueCurrent(element));
return DOM.isElementDirty(element);
}
static isElementDirty(element) {
let isDirty = element.getAttribute(attrValuePrevious) != element.getAttribute(attrValueCurrent);
DOM.handleDirtyElement(element, isDirty);
return isDirty;
}
static handleDirtyElement(element, isDirty) {
DOM.toggleElementHasClassnameFlag(element, isDirty, flagDirty);
}
static toggleElementHasClassnameFlag(element, elementHasFlag, flag) {
let elementAlreadyHasFlag = element.classList.contains(flag);
if (elementHasFlag == elementAlreadyHasFlag) return;
if (elementHasFlag) {
element.classList.add(flag);
} else {
element.classList.remove(flag);
}
}
static hasDirtyChildrenContainer(container) {
if (container == null) return false;
return container.querySelector('.' + flagDirty) != null;
}
static hasDirtyChildrenNotDeletedContainer(container) {
if (container == null || container.classList.contains(flagDelete)) return false;
return container.querySelector('.' + flagDirty + ':not(.' + flagDelete + ', .' + flagDelete + ' *)') != null;
}
static getElementValueCurrent(element) {
let returnVal = '';
if (!Validation.isEmpty(element)) {
if (element.type === "checkbox") {
returnVal = element.checked;
}
/*
else if (element.classList.contains(flagIsDatePicker)) {
returnVal = getDatePickerDate(element, adjust4DayLightSavings);
}
*/
else if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA' || element.tagName === 'SELECT') {
returnVal = element.value;
}
else if (element.tagName === 'BUTTON' && element.classList.contains(flagActive)) {
returnVal = element.classList.contains(flagDelete);
}
else if (element.tagName === 'TD') {
returnVal = DOM.getElementAttributeValueCurrent(element);
}
else {
returnVal = element.textContent;
}
}
if (Validation.isEmpty(returnVal)) returnVal = '';
return returnVal;
}
static getElementAttributeValueCurrent(element) {
debugger;
if (Validation.isEmpty(element)) return null;
return element.getAttribute(attrValueCurrent);
}
static getElementAttributeValuePrevious(element) {
if (Validation.isEmpty(element)) return null;
return element.getAttribute(attrValuePrevious);
}
/* base_table.handleChangeElementCellTable
static updateAndCheckIsTableElementDirty(element) {
let wasDirty = DOM.isElementDirty(element);
let row = DOM.getRowFromElement(element);
let wasDirtyRow = DOM.hasDirtyChildrenNotDeletedContainer(row);
let isDirty = DOM.updateAndCheckIsElementDirty(element);
let cell = DOM.getCellFromElement(element);
Utils.consoleLogIfNotProductionEnvironment({element, row, cell, isDirty, wasDirty});
if (isDirty != wasDirty) {
DOM.handleDirtyElement(cell, isDirty);
let isDirtyRow = DOM.hasDirtyChildrenNotDeletedContainer(row);
Utils.consoleLogIfNotProductionEnvironment({isDirtyRow, wasDirtyRow});
if (isDirtyRow != wasDirtyRow) {
DOM.handleDirtyElement(row, isDirtyRow);
}
}
}
*/
static scrollToElement(parent, element) {
// REQUIRED: parent has scroll-bar
parent.scrollTop(parent.scrollTop() + (element.offset().top - parent.offset().top));
}
static isElementInContainer(container, element) {
if (typeof jQuery === 'function') {
if (container instanceof jQuery) container = container[0];
if (element instanceof jQuery) element = element[0];
}
var containerBounds = container.getBoundingClientRect();
var elementBounds = element.getBoundingClientRect();
return (
containerBounds.top <= elementBounds.top &&
containerBounds.left <= elementBounds.left &&
((elementBounds.top + elementBounds.height) <= (containerBounds.top + containerBounds.height)) &&
((elementBounds.left + elementBounds.width) <= (containerBounds.left + containerBounds.width))
);
}
static alertError(errorType, errorText) {
alert(errorType + '\n' + errorText);
}
static createOptionUnselectedProductVariation() {
return {
[flagProductVariationType]: {
[flagNameAttrOptionText]: [flagName],
[flagNameAttrOptionValue]: [attrIdProductVariationType],
[flagName]: 'Select Variation Type',
[attrIdProductVariationType]: 0,
},
[flagProductVariation]: {
[flagNameAttrOptionText]: [flagName],
[flagNameAttrOptionValue]: [attrIdProductVariation],
[flagName]: 'Select Variation',
[attrIdProductVariation]: 0,
},
};
}
static createOption(optionJson) {
if (Validation.isEmpty(optionJson)) optionJson = {
text: 'Select',
value: 0,
};
let option = document.createElement('option');
option.value = optionJson.value;
option.textContent = optionJson.text;
option.selected = optionJson.selected;
return option;
}
}

View File

@@ -0,0 +1,29 @@
import Utils from '../utils.js';
export default class BusinessObjects {
static getOptionJsonFromObjectJsonAndKeys(objectJson, keyText, keyValue, valueSelected = null) {
return {
text: objectJson[keyText],
value: objectJson[keyValue],
selected: (objectJson[keyValue] == valueSelected),
};
}
static getOptionJsonFromObjectJson(objectJson, valueSelected = null) {
let keyText = objectJson[flagNameAttrOptionText];
let keyValue = objectJson[flagNameAttrOptionValue];
Utils.consoleLogIfNotProductionEnvironment({objectJson, keyText, keyValue});
return BusinessObjects.getOptionJsonFromObjectJsonAndKeys(objectJson, keyText, keyValue, valueSelected);
}
static getObjectText(objectJson) {
return objectJson == null ? '' : objectJson[objectJson[flagNameAttrOptionText]];
}
static getListObjectsFromIdDictAndCsv(idDict, idCsv) {
let listObjects = [];
let ids = idCsv.split(',');
for (let id of ids) {
listObjects.push(idDict[id]);
}
return listObjects;
}
}

View File

@@ -0,0 +1,52 @@
export default class ProductPermutation {
static getProductVariationsFromIdCsv(csvVariations) {
let productVariations = [];
if (!csvVariations) return productVariations;
let variationPairs = csvVariations.split(',');
if (variationPairs.length == 0) return productVariations;
let parts;
variationPairs.forEach((variationPair) => {
parts = variationPair.split(':');
if (parts.length == 2) {
let productVariationType = productVariationTypes[parts[0]];
productVariationType[flagProductVariations].some((productVariation) => {
if (productVariation[attrIdProductVariation] == parts[1]) {
productVariations.push([productVariationType, productVariation]);
return true;
}
return false;
});
}
});
return productVariations;
}
static getProductVariationsPreviewFromIdCsv(csvVariations) {
let variationPairs = ProductPermutation.getProductVariationsFromIdCsv(csvVariations);
let preview = '';
if (variationPairs.length == 0) return preview;
let variationType, variation;
variationPairs.forEach((variationPair) => {
if (preview.length > 0) {
preview += '\n';
}
variationType = variationPair[0];
variation = variationPair[1];
preview += variationType[flagName] + ': ' + variation[flagName];
});
return preview;
}
static getProductVariationsIdCsvFromVariationTypeList(variationTypeList) {
let csvVariations = '';
if (Validation.isEmpty(variationTypeList)) return csvVariations;
variationTypeList.forEach((variationType) => {
if (csvVariations.length > 0) {
csvVariations += ',';
}
csvVariations += variationType[attrIdProductVariationType] + ':' + variationType[flagProductVariations][0][attrIdProductVariation];
});
return csvVariations;
}
}

46
static/js/lib/common.js Normal file
View File

@@ -0,0 +1,46 @@
import Validation from "./validation.js";
export default class Common {
static parseFloatWithDefault(value, defaultValue = 0.00) {
if (!Validation.isEmpty(value) && Validation.isValidNumber(value, true)) {
return parseFloat(value);
}
return defaultValue;
}
static allowClick() {
return !document.querySelectorAll("body").classList.contains(_dataLoadingFlag);
}
static displayOverlay(message, show, force) {
if (show) {
_overlayLoadingCount += 1;
}
else if (force) {
_overlayLoadingCount = 0;
}
else {
_overlayLoadingCount -= 1;
if (_overlayLoadingCount < 0) _overlayLoadingCount = 0;
}
var loadingImg = document.querySelectorAll(idImageLoading);
var overlay = document.querySelectorAll(loadingImg.closest("div.overlay"));
if (_overlayLoadingCount == 0) {
// Prevent short glimpse of prev. content before switch to new content
// caused by data load but not fully rendered
setTimeout(function() {
overlay.fadeOut();
}, 100);
}
else if (show && _overlayLoadingCount == 1) {
// only show once
loadingImg.innerHTML = message;
overlay.style.display = "";
}
}
}

View File

@@ -0,0 +1,5 @@
const _dataLoadingFlag = 'data-loading'
var _domParser = null;
// var hashPageCurrent; // moved to layout
const keyPublicStripe = 'pk_test_51OGrxlL7BuLKjoMpfpfw7bSmZZK1MhqMoQ5VhW2jUj7YtoEejO4vqnxKPiqTHHuh9U4qqkywbPCSI9TpFKtr4SYH007KHMWs68';

10
static/js/lib/events.js Normal file
View File

@@ -0,0 +1,10 @@
export default class Events {
static initialiseEventHandler(selectorElement, classInitialised, eventHandler) {
document.querySelectorAll(selectorElement).forEach(function(element) {
if (element.classList.contains(classInitialised)) return;
element.classList.add(classInitialised);
eventHandler(element);
});
}
}

0
static/js/lib/extras.js Normal file
View File

View File

@@ -0,0 +1,62 @@
import Validation from "./validation.js";
export default class LocalStorage {
/*
function getPageLocalStorage(pageHash) {
let ls;
try {
ls = JSON.parse(localStorage.getItem(pageHash));
} catch {
}
if (Validation.isEmpty(ls)) return {}
return ls;
}
function getPageLocalStorageCurrent() {
return JSON.parse(localStorage.getItem(hashPageCurrent));
}
function setPageLocalStorage(pageHash, newLS) {
localStorage.setItem(pageHash, JSON.stringify(newLS));
}
function clearPageLocalStorage(pageHash) {
localStorage.removeItem(pageHash);
}
function setupPageLocalStorage(pageHash) {
let ls = getPageLocalStorage(pageHash);
if (Validation.isEmpty(ls)) ls = {};
setPageLocalStorage(pageHash, ls);
}
*/
static getLocalStorage(key) {
return JSON.parse(localStorage.getItem(key));
}
static setLocalStorage(key, newLS) {
localStorage.setItem(key, JSON.stringify(newLS));
}
/*
function setupPageLocalStorageNext(pageHashNext) {
let lsOld = getPageLocalStorage(hashPageCurrent);
hashPageCurrent = pageHashNext;
clearPageLocalStorage(hashPageCurrent);
setupPageLocalStorage(hashPageCurrent);
let lsNew = getPageLocalStorage(hashPageCurrent);
lsNew[keyBasket] = (keyBasket in lsOld) ? lsOld[keyBasket] : {'items': []};
setPageLocalStorage(hashPageCurrent, lsNew);
}
*/
}

24
static/js/lib/utils.js Normal file
View File

@@ -0,0 +1,24 @@
// Utility functions
/*
function $(selector) {
return document.querySelector(selector);
}
function $$(selector) {
return document.querySelectorAll(selector);
}
*/
export default class Utils {
static getListFromDict(dict) {
let list = [];
for (let key in dict) {
list.push(dict[key]);
}
return list;
}
static consoleLogIfNotProductionEnvironment(message) {
if (!environment.is_production) {
console.log(message);
}
}
}

158
static/js/lib/validation.js Normal file
View File

@@ -0,0 +1,158 @@
export default class Validation {
/*
isNullOrWhitespace(v) {
let txt = JSON.stringify(v).replace('/\s\g', '');
return (txt == '' || 'null');
}
*/
static isEmpty(object) {
let isEmpty = true;
if (object !== null && object !== "null" && object !== undefined && object !== "undefined") {
if (object.length == undefined) {
isEmpty = false; // object exists but isn't a collection
}
else if (typeof object === "function") {
isEmpty = false; // object is reference
}
else { // string or collection
let isString = (typeof object == "string");
if (isString) object = object.trim();
if (object.length > 0) {
if (isString) {
isEmpty = false; // String greater than length 0
}
else {
if (typeof object[0] != "string") {
isEmpty = false;
}
else {
for(let i = 0; i < object.length; i++) {
if (object[i] != "") {
isEmpty = false;
break
}
}
}
}
}
}
}
return isEmpty;
}
static isValidNumber(value, positiveOnly) {
return !Validation.isEmpty(value) && !isNaN(value) && (!positiveOnly || parseFloat(value) > 0);
}
static getDataContentType(params) {
var data = null;
var contentType = '';
if (!Validation.isEmpty(params)) {
if (typeof params === "string") {
data = params;
contentType = "application/x-www-form-urlencoded; charset=UTF-8";
}
else {
data = JSON.stringify(params);
contentType = "application/json; charset=UTF-8";
}
}
return { Data: data, ContentType: contentType };
}
static arrayContainsItem(array, itemValue) {
var hasItem = false;
if (!Validation.isEmpty(array) && !Validation.isEmpty(itemValue)) {
var isJQueryElementArray = array[0] instanceof jQuery;
if (isJQueryElementArray) {
for (let i = 0; i < array.length; i++) {
if (document.querySelectorAll(array[i]).is(itemValue)) {
hasItem = true;
break;
}
}
}
else {
var isDate = array[0] instanceof Date;
if (isDate) {
for (let i = 0; i < array.length; i++) {
if (array[i].getTime() === itemValue.getTime()) {
hasItem = true;
break;
}
}
}
else {
for (let i = 0; i < array.length; i++) {
if (array[i] == itemValue) {
hasItem = true;
break;
}
}
}
}
}
return hasItem;
}
static dictHasKey(d, k) {
return (k in d);
}
static areEqualDicts(dict1, dict2) {
const keys1 = Object.keys(dict1);
const keys2 = Object.keys(dict2);
if (keys1.length !== keys2.length) {
return false;
}
for (let key of keys1) {
if (dict1[key] !== dict2[key]) {
return false;
}
}
return true;
}
static imageExists(url, callback) {
var img = new Image();
img.onload = function() { callback(true); };
img.onerror = function() { callback(false); };
img.src = url;
}
static toFixedOrDefault(value, decimalPlaces, defaultValue = null) {
return Validation.isValidNumber(value) ? parseFloat(value).toFixed(decimalPlaces) : defaultValue;
}
}

123
static/js/pages/base.js Normal file
View File

@@ -0,0 +1,123 @@
import BusinessObjects from "../lib/business_objects/business_objects.js";
import Events from "../lib/events.js";
import LocalStorage from "../lib/local_storage.js";
import API from "../api.js";
import DOM from "../dom.js";
import Utils from "../lib/utils.js";
import OverlayConfirm from "../components/common/temporary/overlay_confirm.js";
import OverlayError from "../components/common/temporary/overlay_error.js";
export default class BasePage {
constructor(router) {
if (!router) {
throw new Error("Router is required");
}
else {
Utils.consoleLogIfNotProductionEnvironment("initialising with router: ", router);
}
this.router = router;
this.title = titlePageCurrent;
if (this.constructor === BasePage) {
throw new Error("Cannot instantiate abstract class");
}
if (!this.constructor.hash) {
throw new Error(`Class ${this.constructor.name} must have a static hash attribute.`);
}
}
initialize() {
throw new Error("Method 'initialize()' must be implemented.");
}
sharedInitialize() {
this.logInitialisation();
this.hookupCommonElements();
}
logInitialisation() {
Utils.consoleLogIfNotProductionEnvironment('Initializing ' + this.title + ' page');
}
hookupCommonElements() {
// hookupVideos();
this.hookupLogos();
this.hookupOverlays();
}
hookupEventHandler(eventType, selector, callback) {
Events.initialiseEventHandler(selector, flagInitialised, (element) => {
element.addEventListener(eventType, (event) => {
event.stopPropagation();
callback(event, element);
});
});
}
hookupLogos() {
this.hookupEventHandler("click", "." + flagImageLogo + "," + "." + flagLogo, (event, element) => {
Utils.consoleLogIfNotProductionEnvironment('clicking logo');
this.router.navigateToHash(hashPageHome);
});
}
hookupOverlays() {
this.hookupOverlayFromId(idOverlayConfirm);
this.hookupOverlayFromId(idOverlayError);
}
hookupOverlayFromId(idOverlay) {
Events.initialiseEventHandler(idOverlay, flagInitialised, (overlay) => {
overlay.querySelector('button.' + flagCancel).addEventListener("click", (event) => {
event.stopPropagation();
overlay.style.display = 'none';
});
});
}
hookupButtonSave() {
Events.initialiseEventHandler('form.' + flagFilter + ' button.' + flagSave, flagInitialised, (button) => {
button.addEventListener("click", (event) => {
event.stopPropagation();
Utils.consoleLogIfNotProductionEnvironment('saving page: ', this.title);
OverlayConfirm.show();
});
});
}
leave() {
Utils.consoleLogIfNotProductionEnvironment('Leaving ' + this.title + ' page');
if (this.constructor === BasePage) {
throw new Error("Must implement leave() method.");
}
}
setLocalStoragePage(dataPage) {
LocalStorage.setLocalStorage(this.hash, dataPage);
}
getLocalStoragePage() {
return LocalStorage.getLocalStorage(this.hash);
}
toggleShowButtonsSaveCancel(show) { // , buttonSave = null, buttonCancel = null
let buttonSave = document.querySelector('form.' + flagFilter + ' button.' + flagSave);
let buttonCancel = document.querySelector('form.' + flagFilter + ' button.' + flagCancel);
if (show) {
buttonCancel.classList.remove(flagCollapsed);
buttonSave.classList.remove(flagCollapsed);
Utils.consoleLogIfNotProductionEnvironment('showing buttons');
} else {
buttonCancel.classList.add(flagCollapsed);
buttonSave.classList.add(flagCollapsed);
Utils.consoleLogIfNotProductionEnvironment('hiding buttons');
}
}
static isDirtyFilter(filter) {
let isDirty = DOM.updateAndCheckIsElementDirty(filter);
if (isDirty) document.querySelectorAll(idTableMain + ' tbody tr').remove();
return isDirty;
}
}

View File

@@ -0,0 +1,553 @@
import BusinessObjects from "../lib/business_objects/business_objects.js";
import Events from "../lib/events.js";
import LocalStorage from "../lib/local_storage.js";
import Validation from "../lib/validation.js";
import BasePage from "./base.js";
import API from "../api.js";
import DOM from "../dom.js";
import Utils from "../lib/utils.js";
import OverlayConfirm from "../components/common/temporary/overlay_confirm.js";
import OverlayError from "../components/common/temporary/overlay_error.js";
export default class TableBasePage extends BasePage {
// static hash
// static attrIdRowObject
// callSaveTableContent
constructor(router) {
super(router);
this.cursorYInitial = null;
this.rowInitial = null;
this.placeholder = null;
this.dragSrcEl = null;
this.dragSrcRow = null;
this.hookupTableCellDdls = this.hookupTableCellDdls.bind(this);
}
initialize(isPopState = false) {
throw new Error("Must implement initialize() method.");
}
sharedInitialize(isPopState = false, isSinglePageApp = false) {
if (!isPopState) {
super.sharedInitialize();
this.hookupFilters();
this.hookupButtonsSaveCancel();
this.hookupTableMain();
OverlayConfirm.hookup(() => {
if (isSinglePageApp) {
this.saveRecordsTableDirtySinglePageApp();
}
else {
this.saveRecordsTableDirty();
}
});
} else {
let dataPage = this.getLocalStoragePage();
let filters = dataPage[flagFormFilters];
let formFilters = this.getFormFilters();
let filtersDefault = DOM.convertForm2JSON(formFilters);
if (!Validation.areEqualDicts(filters, filtersDefault)) {
this.callFilterTableContent();
}
}
}
hookupFilters() {
if (this.constructor === TableBasePage) {
throw new Error("Subclass of TableBasePage must implement method hookupFilters().");
}
}
sharedHookupFilters() {
this.hookupButtonApplyFilters();
}
hookupFilterActive() {
this.hookupFilter(flagActive);
}
hookupFilter(filterFlag, handler = (event, filter) => { return TableBasePage.isDirtyFilter(filter); }) {
let filterSelector = idFormFilters + ' .' + filterFlag;
this.hookupEventHandler("change", filterSelector, handler);
}
static isDirtyFilter(filter) {
let isDirty = DOM.updateAndCheckIsElementDirty(filter);
if (isDirty) {
let tbody = document.querySelector(idTableMain + ' tbody');
tbody.querySelectorAll('tr').remove();
tbody.appendChild(document.createElement('<div>Press "Apply Filters" to refresh the table.</div>'));
}
return isDirty;
}
hookupFilterIsNotEmpty() {
this.hookupFilter(flagIsNotEmpty);
}
hookupButtonApplyFilters() {
this.hookupEventHandler("click", idButtonApplyFilters, (event, button) => {
event.stopPropagation();
this.callFilterTableContent();
});
}
/*
getAndLoadFilteredTableContent = () => {
this.callFilterTableContent()
.catch(error => console.error('Error:', error));
}
*/
getFormFilters() {
return document.querySelector(idFormFilters);
}
callFilterTableContent() {
let formFilters = this.getFormFilters();
let filtersJson = DOM.convertForm2JSON(formFilters);
this.leave();
API.goToHash(this.constructor.hash, filtersJson);
}
callbackLoadTableContent(response) {
let table = TableBasePage.getTableMain();
let bodyTable = table.querySelector('tbody');
bodyTable.querySelectorAll('tr').forEach(function(row) { row.remove(); });
let rowsJson = response.data[flagRows];
if (!Validation.isEmpty(rowsJson) && rowsJson.every(row => row.hasOwnProperty('display_order'))) {
rowsJson = rowsJson.sort((a, b) => a.display_order - b.display_order);
}
rowsJson.forEach(this.loadRowTable.bind(this));
this.hookupTableMain();
}
static getTableMain() {
return document.querySelector(idTableMain);
}
loadRowTable(rowJson) {
throw new Error("Subclass of TableBasePage must implement method loadRowTable().");
}
getAndLoadFilteredTableContentSinglePageApp() {
this.callFilterTableContent()
.then(data => {
Utils.consoleLogIfNotProductionEnvironment('Table data received:', data);
this.callbackLoadTableContent(data);
})
.catch(error => console.error('Error:', error));
}
hookupButtonsSaveCancel() {
this.hookupButtonSave();
this.hookupButtonCancel();
this.toggleShowButtonsSaveCancel(false);
}
saveRecordsTableDirty() {
let records = this.getTableRecords(true);
if (records.length == 0) {
OverlayError.show('No records to save');
return;
}
let formElement = this.getFormFilters();
let comment = DOM.getElementValueCurrent(document.querySelector(idTextareaConfirm));
this.callSaveTableContent(records, formElement, comment)
.then(data => {
if (data[flagStatus] == flagSuccess) {
if (_verbose) {
Utils.consoleLogIfNotProductionEnvironment('Records saved!');
Utils.consoleLogIfNotProductionEnvironment('Data received:', data);
}
this.callFilterTableContent();
}
else {
Utils.consoleLogIfNotProductionEnvironment("error: ", data[flagMessage]);
OverlayError.show(data[flagMessage]);
}
})
.catch(error => console.error('Error:', error));
}
getTableRecords(dirtyOnly = false) {
let records = [];
let record;
document.querySelectorAll(idTableMain + ' > tbody > tr').forEach((row) => {
if (dirtyOnly && !DOM.hasDirtyChildrenContainer(row)) return;
record = this.getJsonRow(row);
records.push(record);
});
return records;
}
getJsonRow(row) {
throw new Error("Subclass of TableBasePage must implement method getJsonRow().");
}
saveRecordsTableDirtySinglePageApp() {
let records = this.getTableRecords(true);
if (records.length == 0) {
OverlayError.show('No records to save');
return;
}
let formElement = this.getFormFilters();
let comment = DOM.getElementValueCurrent(document.querySelector(idTextareaConfirm));
this.callSaveTableContent(records, formElement, comment)
.then(data => {
if (data[flagStatus] == flagSuccess) {
if (_verbose) {
Utils.consoleLogIfNotProductionEnvironment('Records saved!');
Utils.consoleLogIfNotProductionEnvironment('Data received:', data);
}
this.callbackLoadTableContent(data);
}
else {
Utils.consoleLogIfNotProductionEnvironment("error: ", data[flagMessage]);
OverlayError.show(data[flagMessage]);
}
})
.catch(error => console.error('Error:', error));
}
hookupButtonCancel() {
Events.initialiseEventHandler(idFormFilters + ' button.' + flagCancel, flagInitialised, (button) => {
button.addEventListener("click", (event) => {
event.stopPropagation();
this.callFilterTableContent();
});
button.classList.add(flagCollapsed);
});
}
handleClickAddRowTable(event, button) {
event.stopPropagation();
_rowBlank.setAttribute(this.constructor.attrIdRowObject, -1 - _rowBlank.getAttribute(this.constructor.attrIdRowObject));
let tbody = document.querySelector(idTableMain + ' tbody');
let row = _rowBlank.cloneNode(true);
row.classList.remove(flagInitialised);
row.querySelectorAll('.' + flagInitialised).forEach(function(element) {
element.classList.remove(flagInitialised);
});
let countRows = document.querySelectorAll(idTableMain + ' > tbody > tr').length;
row.setAttribute(this.constructor.attrIdRowObject, -1 - countRows);
this.initialiseRowNew(tbody, row);
tbody.appendChild(row);
this.hookupTableMain();
}
initialiseRowNew(tbody, row) {
if (this.constructor === TableBasePage) {
throw new Error("Subclass of TableBasePage must implement method initialiseRowNew().");
}
row.classList.remove(flagRowNew);
}
hookupTableMain() {
if (this.constructor === TableBasePage) {
throw new Error("Must implement hookupTableMain() method.");
}
if (true) { // _rowBlank == null) {
Events.initialiseEventHandler(idTableMain, flagInitialised, (table) => {
this.cacheRowBlank();
});
}
}
cacheRowBlank() {
let selectorRowNew = idTableMain + ' tbody tr.' + flagRowNew;
let rowBlankTemp = document.querySelector(selectorRowNew);
Utils.consoleLogIfNotProductionEnvironment("row blank temp: ", rowBlankTemp);
let countRows = document.querySelectorAll(idTableMain + ' > tbody > tr').length;
_rowBlank = rowBlankTemp.cloneNode(true);
document.querySelectorAll(selectorRowNew).forEach(function(row) {
row.remove();
});
_rowBlank.setAttribute(this.constructor.attrIdRowObject, -1 - countRows);
}
initialiseSliderDisplayOrderRowNew(tbody, row) {
// let tdSelector = ':scope > tr > td.' + flagDisplayOrder;
// let tbody = document.querySelector('table' + (Validation.isEmpty(flagTable) ? '' : '.' + flagTable) + ' > tbody');
let slidersDisplayOrder = tbody.querySelectorAll(':scope > tr > td.' + flagDisplayOrder + ' input.' + flagSlider);
let maxDisplayOrder = 0;
slidersDisplayOrder.forEach((slider) => {
maxDisplayOrder = Math.max(maxDisplayOrder, parseFloat(DOM.getElementValueCurrent(slider)));
});
let sliderDisplayOrder = row.querySelector('td.' + flagDisplayOrder + ' .' + flagSlider);
DOM.setElementValuesCurrentAndPrevious(sliderDisplayOrder, maxDisplayOrder + 1);
}
hookupSlidersDisplayOrderTable() {
let selectorDisplayOrder = idTableMain + ' tbody tr td.' + flagDisplayOrder + ' input.' + flagSlider + '.' + flagDisplayOrder;
this.hookupChangeHandlerTableCells(selectorDisplayOrder);
}
hookupChangeHandlerTableCells(inputSelector, handler = (event, element) => { this.handleChangeNestedElementCellTable(event, element); }) {
Events.initialiseEventHandler(inputSelector, flagInitialised, (input) => {
input.addEventListener("change", (event) => {
handler(event, input);
});
handler(null, input);
});
}
/*
handleChangeElementCellTable(event, element) {
let row = DOM.getRowFromElement(element);
let td = DOM.getCellFromElement(element);
let wasDirtyRow = DOM.hasDirtyChildrenContainer(row);
let wasDirtyElement = element.classList.contains(flagDirty);
let isDirtyElement = DOM.updateAndCheckIsElementDirty(element);
if (isDirtyElement != wasDirtyElement) {
DOM.handleDirtyElement(td, isDirtyElement);
let isNowDirtyRow = DOM.hasDirtyChildrenContainer(row);
if (isNowDirtyRow != wasDirtyRow) {
DOM.handleDirtyElement(row, isNowDirtyRow);
let rows = this.getTableRecords(true);
let existsDirtyRecord = rows.length > 0;
this.toggleShowButtonsSaveCancel(existsDirtyRecord);
}
}
}
handleChangeElementNestedCellTable(event, element, flagColumnList = [], orderNesting = 1) {
let orderNestingTemp = orderNesting;
let row, td, nestedRowSelector;
while (orderNestingTemp > 0) {
nestedRowSelector = idTableMain;
for (let indexOrderNesting = 0; indexOrderNesting < orderNestingTemp; indexOrderNesting++) {
nestedRowSelector += ' tbody tr';
}
row = DOM.getClosestParent(element, nestedRowSelector);
td = row.querySelector('td.' + flag);
}
let row = DOM.getRowFromElement(element);
let td = DOM.getCellFromElement(element);
let wasDirtyRow = DOM.hasDirtyChildrenContainer(row);
let wasDirtyElement = element.classList.contains(flagDirty);
let isDirtyElement = DOM.updateAndCheckIsElementDirty(element);
if (isDirtyElement != wasDirtyElement) {
DOM.handleDirtyElement(td, isDirtyElement);
let isNowDirtyRow = DOM.hasDirtyChildrenContainer(row);
if (isNowDirtyRow != wasDirtyRow) {
DOM.handleDirtyElement(row, isNowDirtyRow);
let rows = this.getTableRecords(true);
let existsDirtyRecord = rows.length > 0;
this.toggleShowButtonsSaveCancel(existsDirtyRecord);
}
}
}
handleChangeElementSubtableCell(event, element, flagFieldSubtable) {
let rowSubtable = element.closest(idTableMain + ' td.' + flagFieldSubtable + ' tbody tr');
let rowTable = rowSubtable.closest(idTableMain + ' > tbody > tr');
let td = DOM.getCellFromElement(element);
// let tdSubtable = td.closest('td.' + flagFieldSubtable);
let wasDirtyRowSubtable = DOM.hasDirtyChildrenContainer(rowSubtable);
let wasDirtyRowTable = DOM.hasDirtyChildrenContainer(rowTable);
let wasDirtyElement = element.classList.contains(flagDirty);
let isDirtyElement = DOM.updateAndCheckIsElementDirty(element);
Utils.consoleLogIfNotProductionEnvironment({isDirtyElement, wasDirtyElement});
if (isDirtyElement != wasDirtyElement) {
DOM.handleDirtyElement(td, isDirtyElement);
let isNowDirtyRowSubtable = DOM.hasDirtyChildrenContainer(rowSubtable);
Utils.consoleLogIfNotProductionEnvironment({isNowDirtyRowSubtable, wasDirtyRowSubtable});
if (isNowDirtyRowSubtable != wasDirtyRowSubtable) {
DOM.handleDirtyElement(rowSubtable, isNowDirtyRowSubtable);
let isNowDirtyRowTable = DOM.hasDirtyChildrenContainer(rowTable);
Utils.consoleLogIfNotProductionEnvironment({isNowDirtyRowTable, wasDirtyRowTable});
if (isNowDirtyRowTable != wasDirtyRowTable) {
DOM.handleDirtyElement(rowTable, isNowDirtyRowTable);
let rows = this.getTableRecords(true);
let existsDirtyRecord = rows.length > 0;
this.toggleShowButtonsSaveCancel(existsDirtyRecord);
}
}
}
}
*/
handleChangeNestedElementCellTable(event, element) {
let wasDirtyParentRows = this.getAllIsDirtyRowsInParentTree(element);
let wasDirtyElement = element.classList.contains(flagDirty);
let isDirtyElement = DOM.updateAndCheckIsElementDirty(element);
Utils.consoleLogIfNotProductionEnvironment({isDirtyElement, wasDirtyElement, wasDirtyParentRows});
let td = DOM.getCellFromElement(element);
DOM.setElementAttributeValueCurrent(td, DOM.getElementAttributeValueCurrent(element));
if (isDirtyElement != wasDirtyElement) {
DOM.handleDirtyElement(td, isDirtyElement);
this.updateAndToggleShowButtonsSaveCancel();
this.cascadeChangedIsDirtyNestedElementCellTable(element, isDirtyElement, wasDirtyParentRows);
}
}
getAllIsDirtyRowsInParentTree(element) {
let rows = [];
let parent = element;
let isDirty;
while (parent) {
if (parent.matches('tr')) {
isDirty = parent.classList.contains(flagDirty)
rows.push(isDirty);
}
parent = parent.parentElement;
}
return rows;
}
cascadeChangedIsDirtyNestedElementCellTable(element, isDirtyElement, wasDirtyParentRows) {
if (Validation.isEmpty(wasDirtyParentRows)) return;
let td = DOM.getCellFromElement(element);
let isDirtyTd = isDirtyElement || DOM.hasDirtyChildrenContainer(tr);
DOM.handleDirtyElement(td, isDirtyTd);
let tr = DOM.getRowFromElement(td);
let isDirtyRow = isDirtyTd || DOM.hasDirtyChildrenContainer(tr);
let wasDirtyRow = wasDirtyParentRows.shift();
Utils.consoleLogIfNotProductionEnvironment({isDirtyRow, wasDirtyRow});
if (isDirtyRow != wasDirtyRow) {
DOM.handleDirtyElement(tr, isDirtyRow);
this.updateAndToggleShowButtonsSaveCancel();
this.cascadeChangedIsDirtyNestedElementCellTable(tr.parentElement, isDirtyRow, wasDirtyParentRows);
}
}
hookupChangeHandlerTableCellsWhenNotCollapsed(inputSelector, handler = (event, element) => {
if (!element.classList.contains(flagCollapsed)) this.handleChangeNestedElementCellTable(event, element);
}) {
this.hookupEventHandler("change", inputSelector, handler);
}
hookupTextareasCodeTable() {
this.hookupChangeHandlerTableCells(idTableMain + ' tbody tr td.' + flagCode + ' textarea');
}
hookupTextareasNameTable() {
this.hookupChangeHandlerTableCells(idTableMain + ' tbody tr td.' + flagName + ' textarea');
}
hookupTextareasDescriptionTable() {
this.hookupChangeHandlerTableCells(idTableMain + ' tbody tr td.' + flagDescription + ' textarea');
}
hookupFieldsActive(flagTable = '', handleClickRowNew = (event, element) => { this.handleClickAddRowTable(event, element); }) {
let selectorButton = 'table' + (Validation.isEmpty(flagTable) ? '' : '.' + flagTable) + ' > tbody > tr > td.' + flagActive + ' button';
let selectorButtonDelete = selectorButton + '.' + flagDelete;
let selectorButtonUndelete = selectorButton + ':not(.' + flagDelete + ')';
Utils.consoleLogIfNotProductionEnvironment("hookupFieldsActive: ", selectorButtonDelete, selectorButtonUndelete);
this.hookupButtonsRowDelete(selectorButtonDelete, selectorButtonUndelete);
this.hookupButtonsRowUndelete(selectorButtonDelete, selectorButtonUndelete);
this.hookupEventHandler(
"click"
, 'table' + (Validation.isEmpty(flagTable) ? '' : '.' + flagTable) + ' > thead > tr > th.' + flagActive + ' button'
, (event, button) => { handleClickRowNew(event, button); }
);
}
hookupButtonsRowDelete(selectorButtonDelete, selectorButtonUndelete, changeHandler = (event, element) => { this.handleChangeNestedElementCellTable(event, element); }) {
this.hookupEventHandler("click", selectorButtonDelete, (event, element) => {
this.handleClickButtonRowDelete(event, element, selectorButtonDelete, selectorButtonUndelete, (changeEvent, changeElement) => { changeHandler(changeEvent, changeElement); });
});
}
handleClickButtonRowDelete(event, element, selectorButtonDelete, selectorButtonUndelete, changeHandler = (event, element) => { this.handleChangeNestedElementCellTable(event, element); }) {
let row = DOM.getRowFromElement(element);
if (row.classList.contains(flagRowNew) && !DOM.hasDirtyChildrenContainer(row)) {
row.parentNode.removeChild(row);
}
let buttonAdd = element.cloneNode(false);
buttonAdd.classList.remove(flagInitialised);
buttonAdd.classList.remove(flagDelete);
buttonAdd.classList.add(flagAdd);
buttonAdd.textContent = '+';
element.replaceWith(buttonAdd);
changeHandler(null, buttonAdd);
this.hookupButtonsRowUndelete(selectorButtonDelete, selectorButtonUndelete, (changeEvent, changeElement) => { changeHandler(changeEvent, changeElement); });
this.updateAndToggleShowButtonsSaveCancel();
}
hookupButtonsRowUndelete(selectorButtonDelete, selectorButtonUndelete, changeHandler = (event, element) => { this.handleChangeNestedElementCellTable(event, element); }) {
this.hookupEventHandler("click", selectorButtonUndelete, (event, element) => {
this.handleClickButtonRowUndelete(event, element, selectorButtonDelete, selectorButtonUndelete, (changeEvent, changeElement) => { changeHandler(changeEvent, changeElement); });
});
}
handleClickButtonRowUndelete(event, element, selectorButtonDelete, selectorButtonUndelete, changeHandler = (event, element) => { this.handleChangeNestedElementCellTable(event, element); }) {
let buttonDelete = element.cloneNode(false);
buttonDelete.classList.remove(flagInitialised);
buttonDelete.classList.remove(flagAdd);
buttonDelete.classList.add(flagDelete);
buttonDelete.textContent = 'x';
element.replaceWith(buttonDelete);
changeHandler(null, buttonDelete);
this.hookupButtonsRowDelete(selectorButtonDelete, selectorButtonUndelete, (changeEvent, changeElement) => { changeHandler(changeEvent, changeElement); });
this.updateAndToggleShowButtonsSaveCancel();
}
hookupTdsAccessLevel() {
let cellSelector = idTableMain + ' tbody td.' + flagAccessLevel;
this.hookupTableCellDdlPreviews(cellSelector, Utils.getListFromDict(accessLevels));
}
hookupTableCellDdlPreviews(
cellSelector
, optionList
, ddlHookup = (cellSelector) => { this.hookupTableCellDdls(cellSelector); }
, changeHandler = (event, element) => { this.handleChangeNestedElementCellTable(event, element); }
) {
this.hookupEventHandler("click", cellSelector, (event, td) => {
this.handleClickTableCellDdlPreview(
event
, td
, optionList
, cellSelector
, (cellSelector) => { ddlHookup(
cellSelector
, (event, element) => { changeHandler(event, element); }
); }
);
});
ddlHookup(cellSelector + ' select');
}
hookupTableCellDdls(ddlSelector, changeHandler = (event, element) => { this.handleChangeNestedElementCellTable(event, element); }) {
this.hookupEventHandler("change", ddlSelector, (event, element) => { changeHandler(event, element); });
}
handleClickTableCellDdlPreview(event, td, optionObjectList, cellSelector, ddlHookup = (cellSelector) => { this.hookupTableCellDdls(cellSelector); }) {
if (td.querySelector('select')) return;
let tdNew = td.cloneNode(true);
td.parentNode.replaceChild(tdNew, td);
let idSelected = DOM.getElementAttributeValueCurrent(tdNew);
tdNew.innerHTML = '';
let ddl = document.createElement('select');
DOM.setElementValuesCurrentAndPrevious(ddl, idSelected);
let optionJson, option;
if (_verbose) {
Utils.consoleLogIfNotProductionEnvironment("click table cell ddl preview");
Utils.consoleLogIfNotProductionEnvironment({optionObjectList, cellSelector});
}
option = DOM.createOption(null);
ddl.appendChild(option);
optionObjectList.forEach((optionObjectJson) => {
optionJson = BusinessObjects.getOptionJsonFromObjectJson(optionObjectJson, idSelected);
option = DOM.createOption(optionJson);
ddl.appendChild(option);
});
tdNew.appendChild(ddl);
let ddlSelector = cellSelector + ' select';
ddlHookup(ddlSelector);
}
hookupTableCellDDlPreviewsWhenNotCollapsed(cellSelector, optionList, ddlHookup = (event, element) => { this.hookupTableCellDdls(event, element); }) {
this.hookupEventHandler("click", cellSelector, (event, td) => {
let div = td.querySelector('div');
if (!div || div.classList.contains(flagCollapsed)) return;
this.handleClickTableCellDdlPreview(event, td, optionList, cellSelector, (event, element) => { ddlHookup(event, element); });
});
}
toggleColumnCollapsed(flagColumn, isCollapsed) {
this.toggleColumnHasClassnameFlag(flagColumn, isCollapsed, flagCollapsed);
}
toggleColumnHeaderCollapsed(flagColumn, isCollapsed) {
this.toggleColumnHasClassnameFlag(flagColumn, isCollapsed, flagCollapsed);
}
createTdActive(isActive) {
let tdActive = document.createElement("td");
tdActive.classList.add(flagActive);
let buttonActive = document.createElement("button");
buttonActive.classList.add(flagActive);
buttonActive.classList.add(isActive ? flagDelete : flagAdd);
buttonActive.textContent = isActive ? 'x' : '+';
DOM.setElementAttributesValuesCurrentAndPrevious(buttonActive, isActive);
tdActive.appendChild(buttonActive);
return tdActive;
}
leave() {
if (this.constructor === TableBasePage) {
throw new Error("Must implement leave() method.");
}
super.leave();
let formFilters = this.getFormFilters();
let dataPage = {};
dataPage[flagFormFilters] = DOM.convertForm2JSON(formFilters);
this.setLocalStoragePage(dataPage);
}
toggleColumnHasClassnameFlag(columnFlag, isRequiredFlag, classnameFlag) {
let table = TableBasePage.getTableMain();
let columnTh = table.querySelector('th.' + columnFlag);
let columnThHasFlag = columnTh.classList.contains(classnameFlag);
if (isRequiredFlag == columnThHasFlag) return;
DOM.toggleElementHasClassnameFlag(columnTh, isRequiredFlag, classnameFlag);
}
toggleColumnHeaderHasClassnameFlag(columnFlag, isRequiredFlag, classnameFlag) {
let table = TableBasePage.getTableMain();
let columnTh = table.querySelector('th.' + columnFlag);
DOM.toggleElementHasClassnameFlag(columnTh, isRequiredFlag, classnameFlag);
}
updateAndToggleShowButtonsSaveCancel() {
let records = this.getTableRecords(true);
let existsDirtyRecord = records.length > 0;
this.toggleShowButtonsSaveCancel(existsDirtyRecord);
}
}

View File

@@ -0,0 +1,16 @@
// internal
import BasePage from "../base.js";
// vendor
import { Altcha } from "../../vendor/altcha.js";
export default class PageContactSuccess extends BasePage {
static hash = hashPageContactSuccess;
constructor(router) {
super(router);
}
initialize() {
this.sharedInitialize();
}
}

View File

@@ -0,0 +1,23 @@
// internal
import BasePage from "../base.js";
// vendor
import { Altcha } from "../../vendor/altcha.js";
export default class PageContact extends BasePage {
static hash = hashPageContact;
constructor(router) {
super(router);
}
initialize() {
this.sharedInitialize();
this.hookupButtonSubmitFormContactUs();
}
hookupButtonSubmitFormContactUs() {
const button = document.querySelector('form input[type="submit"]');
button.classList.add(flagButton);
button.classList.add(flagButtonPrimary);
}
}

View File

@@ -0,0 +1,23 @@
// internal
import BasePage from "../base.js";
// external
export default class PageHome extends BasePage {
static hash = hashPageHome;
constructor(router) {
super(router);
}
initialize() {
this.sharedInitialize();
this.hookupButtonsNavContact();
}
leave() {
super.leave();
}
}

View File

@@ -0,0 +1,17 @@
import BasePage from "../base.js";
export default class PageAccessibilityReport extends BasePage {
static hash = hashPageAccessibilityReport;
constructor(router) {
super(router);
}
initialize() {
this.sharedInitialize();
}
leave() {
super.leave();
}
}

View File

@@ -0,0 +1,17 @@
import BasePage from "../base.js";
export default class PageAccessibilityStatement extends BasePage {
static hash = hashPageAccessibilityStatement;
constructor(router) {
super(router);
}
initialize() {
this.sharedInitialize();
}
leave() {
super.leave();
}
}

View File

@@ -0,0 +1,18 @@
import BasePage from "../base.js";
export default class PageLicense extends BasePage {
static hash = hashPageLicense;
constructor(router) {
super(router);
}
initialize() {
this.sharedInitialize();
}
leave() {
super.leave();
}
}

View File

@@ -0,0 +1,18 @@
import BasePage from "../base.js";
export default class PagePrivacyPolicy extends BasePage {
static hash = hashPagePrivacyPolicy;
constructor(router) {
super(router);
}
initialize() {
this.sharedInitialize();
}
leave() {
super.leave();
}
}

View File

@@ -0,0 +1,17 @@
import BasePage from "../base.js";
export default class PageRetentionSchedule extends BasePage {
static hash = hashPageDataRetentionSchedule;
constructor(router) {
super(router);
}
initialize() {
this.sharedInitialize();
}
leave() {
super.leave();
}
}

View File

@@ -0,0 +1,19 @@
import BasePage from "../base.js";
export default class PageUser extends BasePage {
static hash = hashPageUser;
constructor(router) {
super(router);
}
initialize() {
this.sharedInitialize();
this.hookupButtonsNavContact();
}
leave() {
super.leave();
}
}

91
static/js/router.js Normal file
View File

@@ -0,0 +1,91 @@
// Pages
// Core
import PageHome from './pages/core/home.js';
import PageContact from './pages/core/contact.js';
import PageContactSuccess from './pages/core/contact-success.js';
// Legal
import PageAccessibilityReport from './pages/legal/accessibility_report.js';
import PageAccessibilityStatement from './pages/legal/accessibility_statement.js';
import PageLicense from './pages/legal/license.js';
import PagePrivacyPolicy from './pages/legal/privacy_policy.js';
import PageRetentionSchedule from './pages/legal/retention_schedule.js';
import API from './api.js';
import DOM from './dom.js';
import Utils from './lib/utils.js';
export default class Router {
constructor() {
// Pages
this.pages = {};
// Core
this.pages[hashPageHome] = { name: 'PageHome', module: PageHome };
this.pages[hashPageContact] = { name: 'PageContact', module: PageContact };
this.pages[hashPageContactSuccess] = { name: 'PageContact', module: PageContactSuccess };
// Legal
this.pages[hashPageAccessibilityStatement] = { name: 'PageAccessibilityStatement', module: PageAccessibilityStatement };
this.pages[hashPageDataRetentionSchedule] = { name: 'PageDataRetentionSchedule', module: PageRetentionSchedule };
this.pages[hashPageLicense] = { name: 'PageLicense', module: PageLicense };
this.pages[hashPagePrivacyPolicy] = { name: 'PagePrivacyPolicy', module: PagePrivacyPolicy };
// Routes
this.routes = {};
// Core
this.routes[hashPageHome] = (isPopState = false) => this.navigateToHash(hashPageHome, isPopState);
this.routes[hashPageContact] = (isPopState = false) => this.navigateToHash(hashPageContact, isPopState);
// Legal
this.routes[hashPageAccessibilityStatement] = (isPopState = false) => this.navigateToHash(hashPageAccessibilityStatement, isPopState);
this.routes[hashPageDataRetentionSchedule] = (isPopState = false) => this.navigateToHash(hashPageDataRetentionSchedule, isPopState);
this.routes[hashPageLicense] = (isPopState = false) => this.navigateToHash(hashPageLicense, isPopState);
this.routes[hashPagePrivacyPolicy] = (isPopState = false) => this.navigateToHash(hashPagePrivacyPolicy, isPopState);
this.initialize();
}
loadPage(hashPage, isPopState = false) {
const PageClass = this.getClassPageFromHash(hashPage);
this.currentPage = new PageClass(this);
this.currentPage.initialize(isPopState);
window.addEventListener('beforeunload', () => this.currentPage.leave());
}
getClassPageFromHash(hashPage) {
let pageJson = this.pages[hashPage];
try {
const module = pageJson.module;
return module;
}
catch (error) {
Utils.consoleLogIfNotProductionEnvironment("this.pages: ", this.pages);
console.error('Page not found:', hashPage);
throw error;
}
}
initialize() {
window.addEventListener('popstate', this.handlePopState.bind(this));
}
handlePopState(event) {
this.loadPageCurrent();
}
loadPageCurrent() {
const hashPageCurrent = DOM.getHashPageCurrent();
this.loadPage(hashPageCurrent);
}
navigateToHash(hash, data = null, params = null, isPopState = false) {
let url = API.getUrlFromHash(hash, params);
history.pushState({data: data, params: params}, '', hash);
API.goToUrl(url, data);
}
navigateToUrl(url, data = null, appendHistory = true) {
// this.beforeLeave();
if (appendHistory) history.pushState(data, '', url);
url = API.parameteriseUrl(url, data);
API.goToUrl(url);
}
static loadPageBodyFromResponse(response) {
DOM.loadPageBody(response.data);
}
}
export const router = new Router();

View File

View File

2636
static/js/vendor/altcha.js vendored Normal file

File diff suppressed because it is too large Load Diff