Move all "edit.js" functions into Modules

Create new repository to track these
This commit is contained in:
2025-03-07 14:46:38 +09:00
commit 55972b4ab7
26 changed files with 5052 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules/

39
ReadMe.md Normal file
View File

@@ -0,0 +1,39 @@
# JavaScript Utils
Collection of Javascript functions. This will build into a single js file that was formaly known as "edit.js" (edit.jq.js).
## Build
Build with the following commands, the output will be stored in "build/js/utilsAll.js" and "build/js/utilsAll.min.js"
For full not minified version
```sh
npm build utilsAll-build
```
For minified build (but keeps the function names as is)
```sh
npm build utilsAll-min-build
```
Or build both at the same time
```sh
npm build utilsAll-build-all
```
## Develop
Install the core npm modules
```sh
npm install
```
Run watch
```sh
npm run utilsAll-develop
```

2
build/js/general/jquery-3.6.0.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
build/js/general/jquery.min.js vendored Symbolic link
View File

@@ -0,0 +1 @@
jquery-3.6.0.min.js

2
build/js/output/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

33
build/test.html Normal file
View File

@@ -0,0 +1,33 @@
<!DOCTYPE html>
<head>
<title>JavaScript Test</title>
<script type="text/javascript" src="js/general/jquery.min.js"></script>
<!-- <script type="text/javascript" src="js/general/translateTest-ja_JP.UTF-8.js"></script> -->
<script type="text/javascript" src="js/output/utilsAll.min.js"></script>
</head>
<body>
<div>
<h1>JavaScript tests</h1>
<div id="test-div">
</div>
</div>
</body>
<script languagae="JavaScript">
document.addEventListener('DOMContentLoaded', function() {
console.log('MAIN PAGE LOADED');
let el = document.getElementById('test-div');
if (el === null) {
throw new Error("element test-div not found");
}
let bytes = 1021152;
//
el.html = '';
el.html += '<div>randomIdF: ' + randomIdF() + '</div>';
el.html += '<div>getWindowSize: ' + getWindowSize() + '</div>';
el.html += '<div>formatBytes: ' + formatBytes(bytes) + '</div>';
el.html += '<div>formatBytesLong: ' + formatBytesLong(bytes) + '</div>';
// console.log('TR: %s', l10n.__('Original'));
// console.log('TR: %s', l10n.__('Not exists'));
});
</script>

59
eslint.config.mjs Normal file
View File

@@ -0,0 +1,59 @@
import globals from 'globals';
import pluginJs from '@eslint/js';
/*
module.exports = {
// in globals block
'extends': 'eslint:recommended',
'parserOptions': {
'ecmaVersion': 6
},
// rules copied
};
*/
/** @type {import('eslint').Linter.Config[]} */
export default [
{languageOptions: {
globals: {
...globals.browser,
...globals.jquery
}
}},
pluginJs.configs.recommended,
{
'rules': {
'indent': [
'error',
'tab',
{
'SwitchCase': 1
}
],
'linebreak-style': [
'error',
'unix'
],
// 'quotes': [
// 'error',
// 'single'
// ],
'semi': [
'error',
'always'
],
'no-console': 'off',
'no-unused-vars': [
'error', {
'vars': 'all',
'args': 'after-used',
'ignoreRestSiblings': false
}
],
// Requires eslint >= v8.14.0
'no-constant-binary-expression': 'error'
}
}
];
// __END__

17
jsconfig.json Normal file
View File

@@ -0,0 +1,17 @@
// https://www.typescriptlang.org/tsconfig/#compilerOptions
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "Node",
"target": "ES2020",
"jsx": "react",
"checkJs": true,
"allowImportingTsExtensions": true,
"strictNullChecks": true,
"strictFunctionTypes": true
},
"exclude": [
"node_modules",
"**/node_modules/*"
]
}

1554
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "javascript-utils",
"version": "1.0.0",
"main": "",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"utilsAll-min-build": "node_modules/esbuild/bin/esbuild utilsAll.min=src/utilsAll.mjs --outdir=build/js/output/ --format=esm --bundle --charset=utf8 --tree-shaking=false --minify-whitespace --minify-syntax --sourcemap",
"utilsAll-build": "node_modules/esbuild/bin/esbuild utilsAll=src/utilsAll.mjs --outdir=build/js/output/ --format=esm --bundle --charset=utf8 --tree-shaking=false",
"utilsAll-build-all": "npm run utilsAll-min-build && npm run utilsAll-build",
"utilsAll-develop": "node_modules/esbuild/bin/esbuild utilsAll=src/utilsAll.mjs --outdir=build/js/output/ --format=esm --bundle --charset=utf8 --tree-shaking=false --watch"
},
"author": "Clemens Schwaighofer",
"license": "",
"description": "Javascript Utils Collection",
"devDependencies": {
"@eslint/js": "^9.20.0",
"esbuild": "^0.25.0",
"eslint": "^9.20.1",
"globals": "^15.15.0"
}
}

438
src/utils/ActionBox.mjs Normal file
View File

@@ -0,0 +1,438 @@
/*
Description: Action Box handling
Date: 2025/3/7
Creator: Clemens Schwaighofer
*/
export { ActionBox };
import { keyInObject, getObjectCount } from './JavaScriptHelpers.mjs';
import { exists } from './DomHelpers.mjs';
import { setCenter, getWindowSize } from './ResizingAndMove.mjs';
class ActionBox {
// open overlay boxes counter for z-index
zIndex = {
base: 100,
max: 110,
indicator: 0,
boxes: {},
active: [],
top: ''
};
// general action box storage
action_box_storage = {};
// set to 10 min (*60 for seconds, *1000 for microseconds)
action_box_cache_timeout = 10 * 60 * 1000;
hec;
l10n;
/**
* action box creator
* @param {Object} hec HtmlElementCreator
* @param {Object} l10n l10nTranslation
*/
constructor(hec, l10n)
{
this.hec = hec;
this.l10n = l10n;
}
/**
* Show an action box
* @param {string} [target_id='actionBox'] where to attach content to, if not exists, create new
* @param {string} [content=''] content to add to the box
* @param {array} [action_box_css=[]] additional css elements for the action box
* @param {number} [override=0] override size adjust
* @param {number} [content_override=0] override content size adjust
*/
showFillActionBox(target_id = 'actionBox', content = '', action_box_css = [], override = 0, content_override = 0)
{
// fill content
this.fillActionBox(target_id, content, action_box_css);
// show the box
this.showActionBox(target_id, override, content_override);
}
/**
* Fill action box with content, create it if it does not existgs
* @param {string} [target_id='actionBox'] where to attach content to, if not exists, create new
* @param {string} [content=''] content to add to the box
* @param {array} [action_box_css=[]] additional css elements for the action box
*/
fillActionBox(target_id = 'actionBox', content = '', action_box_css = [])
{
// show action box, calc height + center
if (!exists(target_id)) {
// add at the bottom
$('#mainContainer').after(
this.hec.phfo(this.hec.cel('div', target_id, '', ['actionBoxElement', 'hide'].concat(action_box_css)))
);
}
// add the info box
$('#' + target_id).html(content);
}
/**
* Adjust the size of the action box
* @param {string} [target_id='actionBox'] which actionBox to work on
* @param {number} [override=0] override size adjust
* @param {number} [content_override=0] override content size adjust
*/
adjustActionBox(target_id = 'actionBox', override = 0, content_override = 0)
{
// adjust box size
this.adjustActionBoxHeight(target_id, override, content_override);
// center the alert box
setCenter(target_id, true, true);
}
/**
* hide any open action boxes and hide overlay
*/
hideAllActionBoxes()
{
// hide all action boxes that might exist
$('#actionBox, div[id^="actionBox-"].actionBoxElement').hide();
// hideOverlayBoxLayers();
$('#overlayBox').hide();
}
/**
* hide action box, but do not clear content
* DEPRECATED
* @param {string} [target_id='actionBox']
*/
hideActionBox(target_id = 'actionBox')
{
this.closeActionBoxFloat(target_id, false);
}
/**
* Just show and adjust the box
* DEPRECAED
* @param {string} [target_id='actionBox'] which actionBox to work on
* @param {number} [override=0] override size adjust
* @param {number} [content_override=0] override content size adjust
* @param {Boolean} [hide_all=false] if set to true, hide all other action boxes
*/
showActionBox(target_id = 'actionBox', override = 0, content_override = 0, hide_all = true)
{
this.showActionBoxFloat(target_id, override, content_override, hide_all);
}
/**
* close an action box with default clear content
* for just hide use hideActionBox
* DEPRECATED
* @param {String} [target_id='actionBox'] which action box to close, default is set
* @param {Boolean} [clean=true] if set to false will not remove html content, just hide
*/
closeActionBox(target_id = 'actionBox', clean = true)
{
// set the target/content ids
this.closeActionBoxFloat(target_id, clean);
}
/**
* TODO: better stacked action box: OPEN
* @param {string} [target_id='actionBox'] which actionBox to work on
* @param {number} [override=0] override size adjust
* @param {number} [content_override=0] override content size adjust
* @param {boolean} [hide_all=false] if set to true, hide all other action boxes
*/
showActionBoxFloat(target_id = 'actionBox', override = 0, content_override = 0, hide_all = false)
{
if (hide_all === true) {
// hide all action boxes if they are currently open
this.hideAllActionBoxes();
}
// if no box, created if
if (!exists('overlayBox')) {
$('body').prepend(this.hec.phfo(this.hec.cel('div', 'overlayBox', '', ['overlayBoxElement'])));
$('#overlayBox').css('zIndex', this.zIndex.base);
}
// adjust zIndex so its above all other and set action box zindex +1
$('#overlayBox').show();
if (!keyInObject(target_id, this.zIndex.boxes)) {
this.zIndex.boxes[target_id] = this.zIndex.max;
// increase by ten
this.zIndex.max += 10;
} else if (this.zIndex.boxes[target_id] + 10 < this.zIndex.max) {
// see if this is the highest level, if not move up and write no max zIndex
// move it up to be the new top and move the others down
// [loop, order by value]
// current hack, increase max
this.zIndex.boxes[target_id] = this.zIndex.max;
this.zIndex.max += 10;
}
// make sure the overlayBox is one level below this
// unless there is an active "indicator" index
if (!this.zIndex.indicator) {
$('#overlayBox').css('zIndex', this.zIndex.boxes[target_id] - 1);
}
$('#' + target_id).css('zIndex', this.zIndex.boxes[target_id]).show();
// set target to this new level
// @ts-ignore
if (this.zIndex.active.indexOf(target_id) == -1) {
// @ts-ignore
this.zIndex.active.push(target_id);
}
this.zIndex.top = target_id;
// adjust size
this.adjustActionBox(target_id, override, content_override);
}
/**
* TODO: better stacked action box: CLOSE
* @param {String} [target_id='actionBox'] which action box to close, default is set
* @param {Boolean} [clean=true] if set to false will not remove html content, just hide
*/
closeActionBoxFloat(target_id = 'actionBox', clean = true)
{
// do nothing if this does not exist
if (!exists(target_id)) {
return;
}
// clear storage object
if (
keyInObject(target_id, this.action_box_storage) && clean === true
) {
this.action_box_storage[target_id] = {};
}
if (clean === true) {
$('#' + target_id).html('');
}
$('#' + target_id).hide();
// remove from active list
// @ts-ignore
let idx = this.zIndex.active.indexOf(target_id);
this.zIndex.active.splice(idx, 1);
// do we have any visible action boxes.
// find the highest zIndex and set overlayBox to this -1
// @ts-ignore
let visible_zIndexes = $('#actionBox:visible, div[id^="actionBox-"].actionBoxElement:visible').map((i, el) => ({
id: el.id,
zIndex: $('#' + el.id).css('zIndex')
})).get();
if (visible_zIndexes.length > 0) {
let max_zIndex = 0;
let max_el_id = '';
for (let zIndex_el of visible_zIndexes) {
if (parseInt(zIndex_el.zIndex) > max_zIndex) {
max_zIndex = parseInt(zIndex_el.zIndex);
max_el_id = zIndex_el.id;
}
}
$('#overlayBox').css('zIndex', max_zIndex - 1);
this.zIndex.top = max_el_id;
} else {
$('#overlayBox').hide();
}
}
/**
* create a new action box and fill it with basic elements
* @param {String} [target_id='actionBox']
* @param {String} [title='']
* @param {Object} [contents={}]
* @param {Object} [headers={}]
* @param {Boolean} [show_close=true]
* @param {Object} [settings={}] Optional settings, eg style sheets
*/
createActionBox(
target_id = 'actionBox',
title = '',
contents = {},
headers = {},
settings = {},
show_close = true
) {
if (!keyInObject(target_id, this.action_box_storage)) {
this.action_box_storage[target_id] = {};
}
// settings can have the following
// : header_css:[]
// : action_box_css:[]
let header_css = [];
if (keyInObject('header_css', settings)) {
header_css = settings.header_css;
}
let action_box_css = [];
if (keyInObject('action_box_css', settings)) {
action_box_css = settings.action_box_css;
}
let elements = [];
// add title + close button to actionBox
elements.push(this.hec.phfo(
this.hec.aelx(this.hec.cel('div', target_id + '_title', '', ['actionBoxTitle', 'flx-spbt'].concat(header_css)),
...show_close === true ? [
// title
this.hec.cel('div', '', title, ['fs-b', 'w-80']),
// close button
this.hec.aelx(this.hec.cel('div', target_id + '_title_close_button', '', ['w-20', 'tar']),
this.hec.cel('input', target_id + '_title_close', '', ['button-close', 'fs-s'],
{
type: 'button',
value: this.l10n.__('Close'),
OnClick: 'closeActionBox(\'' + target_id + '\', false);'
}
)
)
] : [
this.hec.cel('div', '', title, ['fs-b', 'w-100'])
]
)
));
// if we have header content, add that here
if (getObjectCount(headers) > 0) {
// if the element has an entry called "raw_string" then this does not need to be converted
if (keyInObject('raw_string', headers)) {
elements.push(headers.raw_string);
} else {
elements.push(this.hec.phfo(headers));
}
}
// main content part (this should NOT be empty), if empty, add empty _content block
if (getObjectCount(contents) > 0) {
// if the element has an entry called "raw_string" then this does not need to be converted
if (keyInObject('raw_string', contents)) {
elements.push(contents.raw_string);
} else {
elements.push(this.hec.phfo(contents));
}
} else {
elements.push(this.hec.phfo(this.hec.cel('div', target_id + '_content', '', [])));
}
// footer clear call
elements.push(this.hec.phfo(
this.hec.aelx(this.hec.cel('div', target_id + '_footer', '', ['pd-5', 'flx-spbt']),
...show_close === true ? [
// dummy spacer
this.hec.cel('div', '', '', ['fs-b', 'w-80']),
// close button
this.hec.aelx(this.hec.cel('div', target_id + '_footer_close_button', '', ['tar', 'w-20']),
this.hec.cel('input', target_id + '_footer_close', '', ['button-close', 'fs-s'],
{
type: 'button',
value: this.l10n.__('Close'),
OnClick: 'closeActionBox(\'' + target_id + '\', false);'
}
)
)
] : [
this.hec.cel('div', '', '', ['fs-b', 'w-100'])
]
)
));
elements.push(this.hec.phfo(this.hec.cel('input', target_id + '-cache_time', '', [], {
type: 'hidden',
value: Date.now()
})));
this.fillActionBox(target_id, elements.join(''), action_box_css);
}
/**
* adjusts the action box height based on content and window height of browser
* TODO: border on outside/and other margin things need to be added in overall adjustment
* @param {String} [target_id='actionBox'] target id, if not set, fall back to default
* @param {Number} [override=0] override value to add to the actionBox height
* @param {Number} [content_override=0] override the value from _content block if it exists
*/
adjustActionBoxHeight(target_id = 'actionBox', override = 0, content_override = 0)
{
var new_height = 0;
var dim = {};
var abc_dim = {};
var content_id = '';
// make sure it is a number
if (isNaN(override)) {
override = 0;
}
if (isNaN(content_override)) {
content_override = 0;
}
// set the target/content ids
switch (target_id) {
case 'actionBox':
content_id = 'action_box';
break;
case 'actionBoxSub':
content_id ='action_box_sub';
break;
default:
content_id = target_id;
break;
}
// first remove any height, left, top style entris from target and content blocks
// @ts-ignore
$.each([target_id, content_id + '_content'], function(i, v) {
$('#' + v).css({
'height': '',
'width': ''
});
});
if (exists(content_id + '_title')) {
dim.height = $('#' + content_id + '_title').outerHeight();
console.log('Target: %s, Action box Title: %s', target_id, dim.height);
new_height += dim.height ?? 0;
}
if (exists(content_id + '_header')) {
dim.height = $('#' + content_id + '_header').outerHeight();
console.log('Target: %s, Action box Header: %s', target_id, dim.height);
new_height += dim.height ?? 0;
}
if (exists(content_id + '_content')) {
if (content_override > 0) {
console.log('Target: %s, Action box Content Override: %s', target_id, content_override);
new_height += content_override;
} else {
abc_dim.height = $('#' + content_id + '_content').outerHeight();
console.log('Target: %s, Action box Content: %s', target_id, abc_dim.height);
new_height += abc_dim.height ?? 0;
}
}
// always there sets
if (exists(content_id + '_footer')) {
dim.height = $('#' + content_id + '_footer').outerHeight();
console.log('Target: %s, Action box Footer: %s', target_id, dim.height);
new_height += dim.height ?? 0;
}
// get difference for the rest from outer box
// console.log('Target: %s, Action box outer: %s, Content: %s, New: %s', target_id, $('#' + target_id).outerHeight(), $('#' + content_id + '_content').outerHeight(), new_height);
// new_height += ($('#' + target_id).outerHeight() - new_height) + override;
new_height += override;
// get border width top-bottom from action Box, we need to remove this from the final height
// console.log('Target: %s, Border top: %s', target_id, $('#' + target_id).css('border-top-width'));
// get window size and check if content is bigger
var viewport = getWindowSize();
if (new_height >= viewport.height) {
// resize the action box content and set overflow [of-s-y]
if (exists(content_id + '_content')) {
if (!$('#' + content_id + '_content').hasClass('of-s-y')) {
$('#' + content_id + '_content').addClass('of-s-y');
}
}
console.log('Target: %s, Viewport: %s, ActionBox (NH): %s, ABcontent: %s, ABouter: %s', target_id, viewport.height, new_height, abc_dim.height, $('#' + target_id).outerHeight());
// the height off window - all - action box gives new action box height
var m_height = viewport.height - (new_height - (abc_dim.height ?? 0));
console.log('Target: %s, New ABcontent: %s', target_id, m_height);
$('#' + content_id + '_content').css('height', m_height + 'px');
new_height = new_height - (abc_dim.height ?? 0) + m_height;
console.log('Target: %s, New Hight: %s', target_id, new_height);
} else {
// if size ok, check if overflow scoll is set, remove it
if (exists(content_id + '_content')) {
if ($('#' + content_id + '_content').hasClass('of-s-y')) {
$('#' + content_id + '_content').removeClass('of-s-y');
}
}
}
console.log('Target: %s, Action Box new height: %s px (override %s px, content override %s px), window height: %s px, Visible Height: %s px', target_id, new_height, override, content_override, viewport.height, $('#' + content_id).outerHeight());
// adjust height
$('#' + target_id).css('height', new_height + 'px');
}
}
// __EMD__

View File

@@ -0,0 +1,305 @@
/*
Description: Action Indicator and Overlay
Date: 2025/2/7
Creator: Clemens Schwaighofer
*/
import { setCenter } from './ResizingAndMove.mjs';
export {
ActionIndicatorOverlayBox,
actionIndicator, actionIndicatorShow, actionIndicatorHide, overlayBoxShow,
overlayBoxHide, setOverlayBox, hideOverlayBox, ClearCall
};
/*************************************************************
* OLD action indicator and overlay boxes calls
* DO NOT USE
* actionIndicator -> showActionIndicator
* actionIndicator -> hideActionIndicator
* actionIndicatorShow -> showActionIndicator
* actionIndicatorHide -> hideActionIndicator
* overlayBoxShow -> showOverlayBoxLayers
* overlayBoxHide -> hideOverlayBoxLayers
* setOverlayBox -> showOverlayBoxLayers
* hideOverlayBox -> hideOverlayBoxLayers
* ClearCall -> clearCallActionBox
* ***********************************************************/
/**
* show or hide the "do" overlay
* @param {String} loc location name for action indicator
* default empty. for console.log
* @param {Boolean} [overlay=true] override the auto hide/show over the overlay div block
* @deprecated showActionIndicator, hideActionIndicator
*/
function actionIndicator(loc, overlay = true)
{
if ($('#indicator').is(':visible')) {
this.actionIndicatorHide(loc, overlay);
} else {
this.actionIndicatorShow(loc, overlay);
}
}
/**
* explicit show for action Indicator
* instead of automatically show or hide, do on command show
* @param {String} loc location name for action indicator
* default empty. for console.log
* @param {Boolean} [overlay=true] override the auto hide/show over the overlay div block
* @deprecated showActionIndicator, hideActionIndicator
*/
function actionIndicatorShow(loc, overlay = true)
{
// console.log('{Indicator}: SHOW [%s]', loc);
if (!$('#indicator').is(':visible')) {
if (!$('#indicator').hasClass('progress')) {
$('#indicator').addClass('progress');
}
setCenter('indicator', true, true);
$('#indicator').show();
}
if (overlay === true) {
this.overlayBoxShow();
}
}
/**
* explicit hide for action Indicator
* instead of automatically show or hide, do on command hide
* @param {String} loc location name for action indicator
* default empty. for console.log
* @param {Boolean} [overlay=true] override the auto hide/show over the overlay div block
* @deprecated hideActionIndicator
*/
function actionIndicatorHide(loc, overlay = true)
{
// console.log('{Indicator}: HIDE [%s]', loc);
$('#indicator').hide();
if (overlay === true) {
overlayBoxHide();
}
}
/**
* shows the overlay box or if already visible, bumps the zIndex to 100
* @deprecated showOverlayBoxLayers
*/
function overlayBoxShow()
{
// check if overlay box exists and if yes set the z-index to 100
if ($('#overlayBox').is(':visible')) {
$('#overlayBox').css('zIndex', '100');
} else {
$('#overlayBox').show();
$('#overlayBox').css('zIndex', '98');
}
}
/**
* hides the overlay box or if zIndex is 100 bumps it down to previous level
* @deprecated hideOverlayBoxLayers
*/
function overlayBoxHide()
{
// if the overlay box z-index is 100, do no hide, but set to 98
if (parseInt($('#overlayBox').css('zIndex')) >= 100) {
$('#overlayBox').css('zIndex', '98');
} else {
$('#overlayBox').hide();
}
}
/**
* position the overlay block box and shows it
* @deprecated showOverlayBoxLayers
*/
function setOverlayBox()
{
if (!$('#overlayBox').is(':visible')) {
$('#overlayBox').show();
}
}
/**
* opposite of set, always hides overlay box
* @deprecated hideOverlayBoxLayers
*/
function hideOverlayBox()
{
if ($('#overlayBox').is(':visible')) {
$('#overlayBox').hide();
}
}
/**
* the abort call, clears the action box and hides it and the overlay box
* @deprecated clearCallActionBox
*/
function ClearCall()
{
$('#actionBox').html('');
$('#actionBox').hide();
$('#overlayBox').hide();
}
class ActionIndicatorOverlayBox {
// open overlay boxes counter for z-index
#GL_OB_S = 100;
#GL_OB_BASE = 100;
/**
* show action indicator
* - checks if not existing and add
* - only shows if not visible (else ignore)
* - overlaybox check is called and shown on a fixzed
* zIndex of 1000
* - indicator is page centered
* @param {String} loc ID string, only used for console log
*/
showActionIndicator(loc) // eslint-disable-line no-unused-vars
{
// console.log('{Indicator}: SHOW [%s]', loc);
// check if indicator element exists
if ($('#indicator').length == 0) {
var el = document.createElement('div');
el.className = 'progress hide';
el.id = 'indicator';
$('body').append(el);
} else if (!$('#indicator').hasClass('progress')) {
// if I add a class it will not be hidden anymore
// hide it
$('#indicator').addClass('progress').hide();
}
// indicator not visible
if (!$('#indicator').is(':visible')) {
// check if overlay box element exits
this.checkOverlayExists();
// if not visible show
if (!$('#overlayBox').is(':visible')) {
$('#overlayBox').show();
}
// always set to 1000 zIndex to be top
$('#overlayBox').css('zIndex', 1000);
// show indicator
$('#indicator').show();
// center it
setCenter('indicator', true, true);
}
}
/**
* hide action indicator, if it is visiable
* If the global variable GL_OB_S is > GL_OB_BASE then
* the overlayBox is not hidden but the zIndex
* is set to this value
* @param {String} loc ID string, only used for console log
*/
hideActionIndicator(loc) // eslint-disable-line no-unused-vars
{
// console.log('{Indicator}: HIDE [%s]', loc);
// check if indicator is visible
if ($('#indicator').is(':visible')) {
// hide indicator
$('#indicator').hide();
// if global overlay box count is > 0
// then set it to this level and keep
if (this.#GL_OB_S > this.#GL_OB_BASE) {
$('#overlayBox').css('zIndex', this.#GL_OB_S);
} else {
// else hide overlay box and set zIndex to 0
$('#overlayBox').hide();
$('#overlayBox').css('zIndex', this.#GL_OB_BASE);
}
}
}
/**
* checks if overlayBox exists, if not it is
* added as hidden item at the body end
*/
checkOverlayExists()
{
// check if overlay box exists, if not create it
if ($('#overlayBox').length == 0) {
var el = document.createElement('div');
el.className = 'overlayBoxElement hide';
el.id = 'overlayBox';
$('body').append(el);
}
}
/**
* show overlay box
* if not visible show and set zIndex to 10 (GL_OB_BASE)
* if visible, add +1 to the GL_OB_S variable and
* up zIndex by this value
*/
showOverlayBoxLayers(el_id)
{
// console.log('SHOW overlaybox: %s', GL_OB_S);
// if overlay box is not visible show and set zIndex to 0
if (!$('#overlayBox').is(':visible')) {
$('#overlayBox').show();
$('#overlayBox').css('zIndex', this.#GL_OB_BASE);
// also set start variable to 0
this.#GL_OB_S = this.#GL_OB_BASE;
}
// up the overlay box counter by 1
this.#GL_OB_S ++;
// set zIndex
$('#overlayBox').css('zIndex', this.#GL_OB_S);
// if element given raise zIndex and show
if (el_id) {
if ($('#' + el_id).length > 0) {
$('#' + el_id).css('zIndex', this.#GL_OB_S + 1);
$('#' + el_id).show();
}
}
// console.log('SHOW overlaybox NEW zIndex: %s', $('#overlayBox').css('zIndex'));
}
/**
* hide overlay box
* lower GL_OB_S value by -1
* if we are 10 (GL_OB_BASE) or below hide the overlayIndex
* and set zIndex and GL_OB_S to 0
* else just set zIndex to the new GL_OB_S value
* @param {String} el_id Target to hide layer
*/
hideOverlayBoxLayers(el_id='')
{
// console.log('HIDE overlaybox: %s', GL_OB_S);
// remove on layer
this.#GL_OB_S --;
// if 0 or lower (overflow) hide it and
// set zIndex to 0
if (this.#GL_OB_S <= this.#GL_OB_BASE) {
this.#GL_OB_S = this.#GL_OB_BASE;
$('#overlayBox').hide();
$('#overlayBox').css('zIndex', this.#GL_OB_BASE);
} else {
// if OB_S > 0 then set new zIndex
$('#overlayBox').css('zIndex', this.#GL_OB_S);
}
if (el_id) {
$('#' + el_id).hide();
$('#' + el_id).css('zIndex', 0);
}
// console.log('HIDE overlaybox NEW zIndex: %s', $('#overlayBox').css('zIndex'));
}
/**
* only for single action box
*/
clearCallActionBox()
{
$('#actionBox').html('');
$('#actionBox').hide();
this.hideOverlayBoxLayers();
}
}
// __END__

View File

@@ -0,0 +1,19 @@
/*
Description: Date Time functions
Date: 2025//3/6
Creator: Clemens Schwaighofer
*/
export { getTimestamp };
/**
* returns current timestamp (unix timestamp)
* @return {Number} timestamp (in milliseconds)
*/
function getTimestamp()
{
var date = new Date();
return date.getTime();
}
// __END__

75
src/utils/DomHelpers.mjs Normal file
View File

@@ -0,0 +1,75 @@
/*
Description: DOM Helpers
Date: 2025//3/6
Creator: Clemens Schwaighofer
*/
export { loadEl, pop, expandTA, exists };
/**
* Gets html element or throws an error
* @param {string} el_id Element ID to get
* @returns {HTMLElement}
* @throws Error
*/
function loadEl(el_id)
{
let el = document.getElementById(el_id);
if (el === null) {
throw new Error('Cannot find: ' + el_id);
}
return el;
}
/**
* opens a popup window with winName and given features (string)
* @param {String} theURL the url
* @param {String} winName window name
* @param {Object} features popup features
*/
function pop(theURL, winName, features)
{
let __winName = window.open(theURL, winName, features);
if (__winName == null) {
return;
}
__winName.focus();
}
/**
* automatically resize a text area based on the amount of lines in it
* @param {string} ta_id element id
*/
function expandTA(ta_id)
{
let ta = this.loadEl(ta_id);
if (ta instanceof HTMLElement && ta.getAttribute('type') !== "textarea") {
throw new Error("Element is not a textarea: " + ta_id);
}
let maxChars = parseInt(ta.getAttribute('cols') ?? "0");
let ta_value = ta.getAttribute('value');
let theRows = [];
if (ta_value != null) {
theRows = ta_value.split('\n');
}
var numNewRows = 0;
for ( var i = 0; i < theRows.length; i++ ) {
if ((theRows[i].length+2) > maxChars) {
numNewRows += Math.ceil( (theRows[i].length+2) / maxChars ) ;
}
}
ta.setAttribute('row', (numNewRows + theRows.length).toString());
}
/**
* checks if a DOM element actually exists
* @param {String} id Element id to check for
* @return {Boolean} true if element exists, false on failure
*/
function exists(id)
{
return $('#' + id).length > 0 ? true : false;
}
// __END__

80
src/utils/FormatBytes.mjs Normal file
View File

@@ -0,0 +1,80 @@
/*
Description: Byte string formatting
Date: 2025//3/6
Creator: Clemens Schwaighofer
*/
export { formatBytes, formatBytesLong, stringByteFormat };
/**
* converts a int number into bytes with prefix in two decimals precision
* currently precision is fixed, if dynamic needs check for max/min precision
* @param {Number} bytes bytes in int
* @return {String} string in GB/MB/KB
*/
function formatBytes(bytes)
{
var i = -1;
do {
bytes = bytes / 1024;
i++;
} while (bytes > 99);
return (
Math.round(bytes * Math.pow(10, 2)) / Math.pow(10, 2)
) + ['kB', 'MB', 'GB', 'TB', 'PB', 'EB'][i];
}
/**
* like formatBytes, but returns bytes for <1KB and not 0.n KB
* @param {Number} bytes bytes in int
* @return {String} string in GB/MB/KB
*/
function formatBytesLong(bytes)
{
if (isNaN(bytes)) {
return bytes.toString();
}
var i = Math.floor(Math.log(bytes) / Math.log(1024));
var sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
return (
(
bytes /
Math.pow(1024, i)
).toFixed(2)
// * 1 + ' ' + sizes[i]
+ ' ' + sizes[i]
).toString();
}
/**
* Convert a string with B/K/M/etc into a byte number
* @param {String|Number|Object} bytes Any string with B/K/M/etc
* @return {String|Number} A byte number, or original string as is
*/
function stringByteFormat(bytes)
{
// if anything not string return
if (!(typeof bytes === 'string' || bytes instanceof String)) {
return bytes.toString();
}
// for pow exponent list
let valid_units = 'bkmgtpezy';
// valid string that can be converted
let regex = /([\d.,]*)\s?(eb|pb|tb|gb|mb|kb|e|p|t|g|m|k|b)$/i;
let matches = bytes.match(regex);
// if nothing found, return original input
if (matches !== null) {
// remove all non valid entries outside numbers and .
// convert to float number
let m1 = parseFloat(matches[1].replace(/[^0-9.]/,''));
// only get the FIRST letter from the size, convert it to lower case
let m2 = matches[2].replace(/[^bkmgtpezy]/i, '').charAt(0).toLowerCase();
if (m2) {
// use the position in the valid unit list to do the math conversion
bytes = m1 * Math.pow(1024, valid_units.indexOf(m2));
}
}
return bytes;
}
// __END__

View File

@@ -0,0 +1,276 @@
/*
Description: DOM Management and HTML builder
Date: 2025//3/6
Creator: Clemens Schwaighofer
*/
export {
HtmlElementCreator,
// deprecated name
HtmlElementCreator as DomManagement
};
import { deepCopyFunction, isObject } from './JavaScriptHelpers.mjs';
class HtmlElementCreator {
/**
* reates object for DOM element creation flow
* @param {String} tag must set tag (div, span, etc)
* @param {String} [id=''] optional set for id, if input, select will be used for name
* @param {String} [content=''] text content inside, is skipped if sub elements exist
* @param {Array} [css=[]] array for css tags
* @param {Object} [options={}] anything else (value, placeholder, OnClick, style)
* @return {Object} created element as an object
*/
cel(tag, id = '', content = '', css = [], options = {})
{
return {
tag: tag,
id: id,
name: options.name, // override name if set [name gets ignored in tree build anyway]
content: content,
css: css,
options: options,
sub: []
};
}
/**
* attach a cel created object to another to create a basic DOM tree
* @param {Object} base object where to attach/search
* @param {Object} attach the object to be attached
* @param {String} [id=''] optional id, if given search in base for this id and attach there
* @return {Object} "none", technically there is no return needed as it is global attach
*/
ael(base, attach, id = '')
{
if (id) {
// base id match already
if (base.id == id) {
// base.sub.push(Object.assign({}, attach));
base.sub.push(deepCopyFunction(attach));
} else {
// sub check
if (isObject(base.sub) && base.sub.length > 0) {
for (var i = 0; i < base.sub.length; i ++) {
// recursive call to sub element
this.ael(base.sub[i], attach, id);
}
}
}
} else {
// base.sub.push(Object.assign({}, attach));
base.sub.push(deepCopyFunction(attach));
}
return base;
}
/**
* directly attach n elements to one master base element
* this type does not support attach with optional id
* @param {Object} base object to where we attach the elements
* @param {...Object} attach attach 1..n: attach directly to the base element those attachments
* @return {Object} "none", technically there is no return needed, global attach
*/
aelx(base, ...attach)
{
for (var i = 0; i < attach.length; i ++) {
// base.sub.push(Object.assign({}, attach[i]));
base.sub.push(deepCopyFunction(attach[i]));
}
return base;
}
/**
* same as aelx, but instead of using objects as parameters
* get an array of objects to attach
* @param {Object} base object to where we attach the elements
* @param {Array} attach array of objects to attach
* @return {Object} "none", technically there is no return needed, global attach
*/
aelxar(base, attach)
{
for (var i = 0; i < attach.length; i ++) {
// base.sub.push(Object.assign({}, attach[i]));
base.sub.push(deepCopyFunction(attach[i]));
}
return base;
}
/**
* resets the sub elements of the base element given
* @param {Object} base cel created element
* @return {Object} returns reset base element
*/
rel(base)
{
base.sub = [];
return base;
}
/**
* searches and removes style from css array
* @param {Object} _element element to work one
* @param {String} css style sheet to remove (name)
* @return {Object} returns full element
*/
rcssel(_element, css)
{
var css_index = _element.css.indexOf(css);
if (css_index > -1) {
_element.css.splice(css_index, 1);
}
return _element;
}
/**
* adds a new style sheet to the element given
* @param {Object} _element element to work on
* @param {String} css style sheet to add (name)
* @return {Object} returns full element
*/
acssel(_element, css)
{
var css_index = _element.css.indexOf(css);
if (css_index == -1) {
_element.css.push(css);
}
return _element;
}
/**
* removes one css and adds another
* is a wrapper around rcssel/acssel
* @param {Object} _element element to work on
* @param {String} rcss style to remove (name)
* @param {String} acss style to add (name)
* @return {Object} returns full element
*/
scssel(_element, rcss, acss)
{
this.rcssel(_element, rcss);
this.acssel(_element, acss);
}
/**
* parses the object tree created with cel/ael and converts it into an HTML string
* that can be inserted into the page
* @param {Object} tree object tree with dom element declarations
* @return {String} HTML string that can be used as innerHTML
*/
phfo(tree)
{
let name_elements = [
'button',
'fieldset',
'form',
'iframe',
'input',
'map',
'meta',
'object',
'output',
'param',
'select',
'textarea',
];
let skip_options = [
'id',
'name',
'class',
];
let no_close = [
'input',
'br',
'img',
'hr',
'area',
'col',
'keygen',
'wbr',
'track',
'source',
'param',
'command',
// only in header
'base',
'meta',
'link',
'embed',
];
// holds the elements
var content = [];
// main part line
var line = '<' + tree.tag;
var i;
// first id, if set
if (tree.id) {
line += ' id="' + tree.id + '"';
// if anything input (input, textarea, select then add name too)
if (name_elements.includes(tree.tag)) {
line += ' name="' + (tree.name ? tree.name : tree.id) + '"';
}
}
// second CSS
if (isObject(tree.css) && tree.css.length > 0) {
line += ' class="';
for (i = 0; i < tree.css.length; i ++) {
line += tree.css[i] + ' ';
}
// strip last space
line = line.slice(0, -1);
line += '"';
}
// options is anything key = "data"
if (isObject(tree.options)) {
// ignores id, name, class as key
for (const [key, item] of Object.entries(tree.options)) {
if (!skip_options.includes(key)) {
line += ' ' + key + '="' + item + '"';
}
}
}
// finish open tag
line += '>';
// push finished line
content.push(line);
// dive into sub tree to attach sub nodes
// NOTES: we can have content (text) AND sub nodes at the same level
// CONTENT (TEXT) takes preference over SUB NODE in order
if (isObject(tree.sub) && tree.sub.length > 0) {
if (tree.content) {
content.push(tree.content);
}
for (i = 0; i < tree.sub.length; i ++) {
content.push(this.phfo(tree.sub[i]));
}
} else if (tree.content) {
content.push(tree.content);
}
// if not input, image or br, then close
if (
!no_close.includes(tree.tag)
) {
content.push('</' + tree.tag + '>');
}
// combine to string
return content.join('');
}
/**
* Create HTML elements from array list
* as a flat element without master object file
* Is like tree.sub call
* @param {Array} list Array of cel created objects
* @return {String} HTML String
*/
phfa(list)
{
var content = [];
for (var i = 0; i < list.length; i ++) {
content.push(this.phfo(list[i]));
}
return content.join('');
}
}
// __EMD__

217
src/utils/HtmlHelpers.mjs Normal file
View File

@@ -0,0 +1,217 @@
/*
Description: HTML Helpers
Date: 2025//3/6
Creator: Clemens Schwaighofer
*/
export { escapeHtml, unescapeHtml, html_options, html_options_block, html_options_refill };
import { loadEl} from './DomHelpers.mjs';
import { DomManagement } from './HtmlElementCreator.mjs';
let dom = new DomManagement();
/**
* Escapes HTML in string
* @param {String} string Text to escape HTML in
* @returns {String}
*/
function escapeHtml(string)
{
return string.replace(/[&<>"'/]/g, function (s) {
var entityMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
'\'': '&#39;',
'/': '&#x2F;'
};
return entityMap[s];
});
}
/**
* Unescape a HTML encoded string
* @param {String} string Text to unescape HTML in
* @returns {String}
*/
function unescapeHtml(string)
{
return string.replace(/&[#\w]+;/g, function (s) {
var entityMap = {
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&quot;': '"',
'&#39;': '\'',
'&#x2F;': '/'
};
return entityMap[s];
});
}
// BLOCK: html wrappers for quickly creating html data blocks
/**
* NOTE: OLD FORMAT which misses multiple block set
* creates an select/options drop down block.
* the array needs to be key -> value format.
* key is for the option id and value is for the data output
* @param {String} name name/id
* @param {Object} data array for the options
* @param {String} [selected=''] selected item uid
* @param {Boolean} [options_only=false] if this is true, it will not print the select part
* @param {Boolean} [return_string=false] return as string and not as element
* @param {String} [sort=''] if empty as is, else allowed 'keys',
* 'values' all others are ignored
* @return {String} html with build options block
* @deprecated html_options_block
*/
function html_options(name, data, selected = '', options_only = false, return_string = false, sort = '')
{
// wrapper to new call
return this.html_options_block(
name, data, selected, 0, options_only, return_string, sort
);
}
/**
* NOTE: USE THIS CALL, the above one is deprecated
* creates an select/options drop down block.
* the array needs to be key -> value format.
* key is for the option id and value is for the data output
* @param {String} name name/id
* @param {Object} data array for the options
* @param {String} [selected=''] selected item uid
* @param {Number} [multiple=0] if this is 1 or larger, the drop down
* will be turned into multiple select
* the number sets the size value unless it is 1,
* then it is default
* @param {Boolean} [options_only=false] if this is true, it will not print the select part
* @param {Boolean} [return_string=false] return as string and not as element
* @param {String} [sort=''] if empty as is, else allowed 'keys',
* 'values' all others are ignored
* @param {String} [onchange=''] onchange trigger call, default unset
* @return {String} html with build options block
*/
function html_options_block(
name, data, selected = '', multiple = 0, options_only = false, return_string = false, sort = '', onchange = ''
) {
var content = [];
var element_select;
var select_options = {};
var element_option;
var data_list = []; // for sorted output
var value;
var options = {};
// var option;
if (multiple > 0) {
select_options.multiple = '';
if (multiple > 1) {
select_options.size = multiple;
}
}
if (onchange) {
select_options.OnChange = onchange;
}
// set outside select, gets stripped on return if options only is true
element_select = dom.cel('select', name, '', [], select_options);
// console.log('Call for %s, options: %s', name, options_only);
if (sort == 'keys') {
data_list = Object.keys(data).sort();
} else if (sort == 'values') {
data_list = Object.keys(data).sort((a, b) => ('' + data[a]).localeCompare(data[b]));
} else {
data_list = Object.keys(data);
}
// console.log('ORDER: %s', data_list);
// use the previously sorted list
// for (const [key, value] of Object.entries(data)) {
for (const key of data_list) {
value = data[key];
// console.log('create [%s] options: key: %s, value: %s', name, key, value);
// basic options init
options = {
'label': value,
'value': key,
'selected': ''
};
// add selected if matching
if (multiple == 0 && !Array.isArray(selected) && selected == key) {
options.selected = '';
}
// for multiple, we match selected as array
if (multiple == 1 && Array.isArray(selected) && selected.indexOf(key) != -1) {
options.selected = '';
}
// create the element option
element_option = dom.cel('option', '', value, [], options);
// attach it to the select element
dom.ael(element_select, element_option);
}
// if with select part, convert to text
if (!options_only) {
if (return_string) {
content.push(dom.phfo(element_select));
return content.join('');
} else {
return element_select;
}
} else {
// strip select part
if (return_string) {
for (var i = 0; i < element_select.sub.length; i ++) {
content.push(dom.phfo(element_select.sub[i]));
}
return content.join('');
} else {
return element_select.sub;
}
}
}
/**
* refills a select box with options and keeps the selected
* @param {String} name name/id
* @param {Object} data array of options
* @param {String} [sort=''] if empty as is, else allowed 'keys', 'values'
* all others are ignored
*/
function html_options_refill(name, data, sort = '')
{
var element_option;
var option_selected;
var data_list = []; // for sorted output
var value;
// skip if not exists
if (document.getElementById(name)) {
// console.log('Call for %s, options: %s', name, options_only);
if (sort == 'keys') {
data_list = Object.keys(data).sort();
} else if (sort == 'values') {
data_list = Object.keys(data).sort((a, b) => ('' + data[a]).localeCompare(data[b]));
} else {
data_list = Object.keys(data);
}
// first read in existing ones from the options and get the selected one
[].forEach.call(document.querySelectorAll('#' + name + ' :checked'), function(elm) {
option_selected = elm.value;
});
loadEl(name).innerHTML = '';
for (const key of data_list) {
value = data[key];
// console.log('add [%s] options: key: %s, value: %s', name, key, value);
element_option = document.createElement('option');
element_option.label = value;
element_option.value = key;
element_option.innerHTML = value;
if (key == option_selected) {
element_option.selected = true;
}
loadEl(name).appendChild(element_option);
}
}
}
// __EMD__

View File

@@ -0,0 +1,159 @@
/*
Description: JavaScript Helpers
Date: 2025//3/6
Creator: Clemens Schwaighofer
*/
export {
errorCatch, isFunction, executeFunctionByName, isObject, getObjectCount,
keyInObject, getKeyByValue,valueInObject, deepCopyFunction
};
/**
* prints out error messages based on data available from the browser
* @param {Object} err error from try/catch block
*/
function errorCatch(err)
{
// for FF & Chrome
if (err.stack) {
// only FF
if (err.lineNumber) {
console.error('ERROR[%s:%s] ', err.name, err.lineNumber, err);
} else if (err.line) {
// only Safari
console.error('ERROR[%s:%s] ', err.name, err.line, err);
} else {
console.error('ERROR[%s] ', err.name, err);
}
} else if (err.number) {
// IE
console.error('ERROR[%s:%s] %s', err.name, err.number, err.message);
console.error('ERROR[description] %s', err.description);
} else {
// the rest
console.error('ERROR[%s] %s', err.name, err.message);
}
}
/**
* check if name is a function
* @param {string} name Name of function to check if exists
* @return {Boolean} true/false
*/
function isFunction(name)
{
if (typeof window[name] !== 'undefined' &&
typeof window[name] === 'function') {
return true;
} else {
return false;
}
}
/**
* call a function by its string name
* https://stackoverflow.com/a/359910
* example: executeFunctionByName("My.Namespace.functionName", window, arguments);
* @param {string} functionName The function name or namespace + function
* @param {any} context context (window or first namespace)
* hidden next are all the arguments
* @return {any} Return values from functon
*/
function executeFunctionByName(functionName, context /*, args */)
{
var args = Array.prototype.slice.call(arguments, 2);
var namespaces = functionName.split('.');
var func = namespaces.pop();
if (func == undefined) {
throw new Error("Cannot get function from namespaces: " + functionName);
}
for (var i = 0; i < namespaces.length; i++) {
context = context[namespaces[i]];
}
return context[func].apply(context, args);
}
/**
* checks if a variable is an object
* @param {any} val possible object
* @return {Boolean} true/false if it is an object or not
*/
function isObject(val)
{
if (val === null) {
return false;
}
return ((typeof val === 'function') || (typeof val === 'object'));
}
/**
* get the length of an object (entries)
* @param {Object} object object to check
* @return {Number} number of entry
*/
function getObjectCount(object)
{
return Object.keys(object).length;
}
/**
* checks if a key exists in a given object
* @param {String} key key name
* @param {Object} object object to search key in
* @return {Boolean} true/false if key exists in object
*/
function keyInObject(key, object)
{
return Object.prototype.hasOwnProperty.call(object, key) ? true : false;
}
/**
* returns matching key of value
* @param {Object} object object to search value in
* @param {any} value any value (String, Number, etc)
* @return {String} the key found for the first matching value
*/
function getKeyByValue(object, value)
{
return Object.keys(object).find(key => object[key] === value) ?? '';
}
/**
* returns true if value is found in object with a key
* @param {Object} object object to search value in
* @param {any} value any value (String, Number, etc)
* @return {Boolean} true on value found, false on not found
*/
function valueInObject(object, value)
{
return Object.keys(object).find(key => object[key] === value) ? true : false;
}
/**
* true deep copy for Javascript objects
* if Object.assign({}, obj) is not working (shallow)
* or if JSON.parse(JSON.stringify(obj)) is failing
* @param {Object} inObject Object to copy
* @return {Object} Copied Object
*/
function deepCopyFunction(inObject)
{
var outObject, value, key;
if (typeof inObject !== 'object' || inObject === null) {
// Return the value if inObject is not an object
return inObject;
}
// Create an array or object to hold the values
outObject = Array.isArray(inObject) ? [] : {};
// loop over ech entry in object
for (key in inObject) {
value = inObject[key];
// Recursively (deep) copy for nested objects, including arrays
outObject[key] = deepCopyFunction(value);
}
return outObject;
}
// __END__

111
src/utils/LoginMenu.mjs Normal file
View File

@@ -0,0 +1,111 @@
/*
Description: Login access and menu
Date: 2025//3/6
Creator: Clemens Schwaighofer
*/
export { loginLogout, createLoginRow, createNavMenu };
import { isObject, getObjectCount } from './JavaScriptHelpers.mjs';
import { exists } from './DomHelpers.mjs';
import { HtmlElementCreator } from './HtmlElementCreator.mjs';
import { l10nTranslation } from './l10nTranslation.mjs';
let hec = new HtmlElementCreator();
let l10n = new l10nTranslation(i18n ?? {});
/**
* submits basic data for form logout
*/
function loginLogout()
{
const form = document.createElement('form');
form.method = 'post';
const hiddenField = document.createElement('input');
hiddenField.type = 'hidden';
hiddenField.name = 'login_logout';
hiddenField.value = 'Logout';
form.appendChild(hiddenField);
document.body.appendChild(form);
form.submit();
}
/**
* create login string and logout button elements
* @param {String} login_string the login string to show on the left
* @param {String} [header_id='mainHeader'] the target for the main element block
* if not set mainHeader is assumed
* this is the target div for the "loginRow"
*/
function createLoginRow(login_string, header_id = 'mainHeader')
{
// if header does not exist, we do nothing
if (exists(header_id)) {
// that row must exist already, if not it must be the first in the "mainHeader"
if (!exists('loginRow')) {
$('#' + header_id).html(hec.phfo(hec.cel('div', 'loginRow', '', ['loginRow', 'flx-spbt'])));
}
// clear out just in case for first entry
// fill with div name & login/logout button
$('#loginRow').html(hec.phfo(hec.cel('div', 'loginRow-name', login_string)));
$('#loginRow').append(hec.phfo(hec.cel('div', 'loginRow-info', '')));
$('#loginRow').append(hec.phfo(
hec.aelx(
// outer div
hec.cel('div', 'loginRow-logout'),
// inner element
hec.cel('input', 'logout', '', [], {
value: l10n.__('Logout'),
type: 'button',
onClick: 'loginLogout()'
})
)
));
}
}
/**
* create the top nav menu that switches physical between pages
* (edit access data based)
* @param {Object} nav_menu the built nav menu with highlight info
* @param {String} [header_id='mainHeader'] the target for the main element block
* if not set mainHeader is assumed
* this is the target div for the "menuRow"
*/
function createNavMenu(nav_menu, header_id = 'mainHeader')
{
// must be an object
if (isObject(nav_menu) && getObjectCount(nav_menu) > 1) {
// do we have more than one entry, if not, do not show (single page)
if (!exists('menuRow')) {
$('#' + header_id).html(hec.phfo(hec.cel('div', 'menuRow', '', ['menuRow', 'flx-s'])));
}
var content = [];
$.each(nav_menu, function(key, item) {
// key is number
// item is object with entries
if (key != 0) {
content.push(hec.phfo(hec.cel('div', '', '&middot;', ['pd-2'])));
}
// ignore item.popup for now
if (item.enabled) {
// set selected based on window.location.href as the php set will not work
if (window.location.href.indexOf(item.url) != -1) {
item.selected = 1;
}
// create the entry
content.push(hec.phfo(
hec.aelx(
hec.cel('div'),
hec.cel('a', '', item.name, ['pd-2'].concat(item.selected ? 'highlight': ''), {
href: item.url
})
)
));
}
});
$('#menuRow').html(content.join(''));
} else {
$('#menuRow').hide();
}
}
// __END__

50
src/utils/MathHelpers.mjs Normal file
View File

@@ -0,0 +1,50 @@
/*
Description: Math Helpers
Date: 2025/3/6
Creator: Clemens Schwaighofer
*/
export { dec2hex, getRandomIntInclusive, roundPrecision };
/**
* dec2hex :: Integer -> String
* i.e. 0-255 -> '00'-'ff'
* @param {Number} dec decimal string
* @return {String} hex encdoded number
*/
function dec2hex(dec)
{
return ('0' + dec.toString(16)).substring(-2);
}
/**
* generate a number between min/max
* with min/max inclusive.
* eg: 1,5 will create a number ranging from 1 o 5
* @param {Number} min minimum int number inclusive
* @param {Number} max maximumg int number inclusive
* @return {Number} Random number
*/
function getRandomIntInclusive(min, max)
{
min = Math.ceil(min);
max = Math.floor(max);
// The maximum is inclusive and the minimum is inclusive
return Math.floor(Math.random() * (max - min + 1) + min);
}
/**
* Round a number to precision
* @param {Number} number Number to round
* @param {Number} precision Precision value
* @returns {Number}
*/
function roundPrecision(number, precision)
{
if (!isNaN(number) || !isNaN(precision)) {
return number;
}
return Math.round(number * Math.pow(10, precision)) / Math.pow(10, precision);
}
// __END__

View File

@@ -0,0 +1,128 @@
/*
Description: Resize and Move Javascript
Date: 2025//3/6
Creator: Clemens Schwaighofer
*/
import { errorCatch} from './JavaScriptHelpers.mjs';
import { loadEl } from './DomHelpers.mjs';
export { getWindowSize, getScrollOffset, getScrollOffsetOpener, setCenter, goToPos, goTo };
/**
* wrapper to get the real window size for the current browser window
* @return {Object} object with width/height
*/
function getWindowSize()
{
var width, height;
width = window.innerWidth || (window.document.documentElement.clientWidth || window.document.body.clientWidth);
height = window.innerHeight || (window.document.documentElement.clientHeight || window.document.body.clientHeight);
return {
width: width,
height: height
};
}
/**
* wrapper to get the correct scroll offset
* @return {Object} object with x/y px
*/
function getScrollOffset()
{
var left, top;
left = window.pageXOffset || (window.document.documentElement.scrollLeft || window.document.body.scrollLeft);
top = window.pageYOffset || (window.document.documentElement.scrollTop || window.document.body.scrollTop);
return {
left: left,
top: top
};
}
/**
* wrapper to get the correct scroll offset for opener page (from popup)
* @return {Object} object with x/y px
*/
function getScrollOffsetOpener()
{
var left, top;
left = opener.window.pageXOffset || (opener.document.documentElement.scrollLeft || opener.document.body.scrollLeft);
top = opener.window.pageYOffset || (opener.document.documentElement.scrollTop || opener.document.body.scrollTop);
return {
left: left,
top: top
};
}
/**
* centers div to current window size middle
* @param {String} id element to center
* @param {Boolean} left if true centers to the middle from the left
* @param {Boolean} top if true centers to the middle from the top
*/
function setCenter(id, left, top)
{
// get size of id
var dimensions = {
height: $('#' + id).height() ?? 0,
width: $('#' + id).width() ?? 0
};
var type = $('#' + id).css('position');
var viewport = this.getWindowSize();
var offset = this.getScrollOffset();
// console.log('Id %s, type: %s, dimensions %s x %s, viewport %s x %s', id, type, dimensions.width, dimensions.height, viewport.width, viewport.height);
// console.log('Scrolloffset left: %s, top: %s', offset.left, offset.top);
// console.log('Left: %s, Top: %s (%s)', parseInt((viewport.width / 2) - (dimensions.width / 2) + offset.left), parseInt((viewport.height / 2) - (dimensions.height / 2) + offset.top), parseInt((viewport.height / 2) - (dimensions.height / 2)));
if (left) {
$('#' + id).css({
left: (viewport.width / 2) - (dimensions.width / 2) + offset.left + 'px'
});
}
if (top) {
// if we have fixed, we do not add the offset, else it moves out of the screen
var top_pos = type == 'fixed' ?
(viewport.height / 2) - (dimensions.height / 2) :
(viewport.height / 2) - (dimensions.height / 2) + offset.top;
$('#' + id).css({
top: top_pos + 'px'
});
}
}
/**
* goes to an element id position
* @param {String} element element id to move to
* @param {Number} [offset=0] offset from top, default is 0 (px)
* @param {Number} [duration=500] animation time, default 500ms
* @param {String} [base='body,html'] base element for offset scroll
*/
function goToPos(element, offset = 0, duration = 500, base = 'body,html')
{
try {
let element_offset = $('#' + element).offset();
if (element_offset == undefined) {
return;
}
if ($('#' + element).length) {
$(base).animate({
scrollTop: element_offset.top - offset
}, duration);
}
} catch (err) {
errorCatch(err);
}
}
/**
* go to element, scroll
* non jquery
* @param {string} target
*/
function goTo(target)
{
loadEl(target).scrollIntoView({
behavior: 'smooth'
});
}
// __END__

View File

@@ -0,0 +1,49 @@
/*
Description: String Helpers
Date: 2025//3/6
Creator: Clemens Schwaighofer
*/
export { formatString, numberWithCommas, convertLBtoBR };
/**
* simple sprintf formater for replace
* usage: "{0} is cool, {1} is not".format("Alpha", "Beta");
* First, checks if it isn't implemented yet.
* @param {String} string String with {..} entries
* @param {...any} args List of replacement
* @returns {String} Escaped string
*/
function formatString(string, ...args)
{
return string.replace(/{(\d+)}/g, function(match, number)
{
return typeof args[number] != 'undefined' ?
args[number] :
match
;
});
}
/**
* formats flat number 123456 to 123,456
* @param {Number} number number to be formated
* @return {String} formatted with , in thousands
*/
function numberWithCommas(number)
{
var parts = number.toString().split('.');
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return parts.join('.');
}
/**
* converts line breaks to br
* @param {String} string any string
* @return {String} string with <br>
*/
function convertLBtoBR(string)
{
return string.replace(/(?:\r\n|\r|\n)/g, '<br>');
}
// __END__

View File

@@ -0,0 +1,36 @@
/*
Description: Generate unique ids
Date: 2025/3/7
Creator: Clemens Schwaighofer
*/
export { generateId, randomIdF };
/**
* generateId :: Integer -> String
* only works on mondern browsers
* @param {Number} len length of unique id string
* @return {String} random string in length of len
*/
function generateId(len)
{
var arr = new Uint8Array((len || 40) / 2);
(
window.crypto ||
// @ts-ignore
window.msCrypto
).getRandomValues(arr);
return Array.from(arr, self.dec2hex).join('');
}
/**
* creates a pseudo random string of 10 characters
* works on all browsers
* after many runs it will create duplicates
* @return {String} not true random string
*/
function randomIdF()
{
return Math.random().toString(36).substring(2);
}

108
src/utils/UrlParser.mjs Normal file
View File

@@ -0,0 +1,108 @@
/*
Description: HTML Helpers
Date: 2025//3/6
Creator: Clemens Schwaighofer
*/
export { parseQueryString, getQueryStringParam };
import { keyInObject } from './JavaScriptHelpers.mjs';
/**
* parses a query string from window.location.search.substring(1)
* ALTERNATIVE CODE
* var url = new URL(window.location.href);
* param_uid = url.searchParams.get('uid');
* @param {String} [query=''] the query string to parse
* if not set will auto fill
* @param {String} [return_key=''] if set only returns this key entry
* or empty for none
* @return {Object|String} parameter entry list
*/
function parseQueryString(query = '', return_key = '')
{
if (!query) {
query = window.location.search.substring(1);
}
var vars = query.split('&');
var query_string = {};
for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split('=');
var key = decodeURIComponent(pair[0]);
var value = decodeURIComponent(pair[1]);
// skip over run if there is nothing
if (!key || value === 'undefined') {
continue;
}
// If first entry with this name
if (typeof query_string[key] === 'undefined') {
query_string[key] = decodeURIComponent(value);
// If second entry with this name
} else if (typeof query_string[key] === 'string') {
var arr = [query_string[key], decodeURIComponent(value)];
query_string[key] = arr;
// If third or later entry with this name
} else {
query_string[key].push(decodeURIComponent(value));
}
}
if (return_key) {
if (keyInObject(return_key, query_string)) {
return query_string[return_key];
} else {
return '';
}
} else {
return query_string;
}
}
/**
* searches query parameters for entry and returns data either as string or array
* if no search is given the whole parameters are returned as an object
* if a parameter is set several times it will be returned as an array
* if search parameter set and nothing found and empty string is returned
* if no parametes exist and no serach is set and empty object is returned
* @param {String} [search=''] if set searches for this entry, if empty
* all parameters are returned
* @param {String} [query=''] different query string to parse, if not
* set (default) the current window href is used
* @param {Boolean} [single=false] if set to true then only the first found
* will be returned
* @return {Object|Array|String} if search is empty, object, if search is set
* and only one entry, then string, else array
* unless single is true
*/
function getQueryStringParam(search = '', query = '', single = false)
{
if (!query) {
query = window.location.href;
}
const url = new URL(query);
let param = null;
if (search) {
let _params = url.searchParams.getAll(search);
if (_params.length == 1 || single === true) {
param = _params[0];
} else if (_params.length > 1) {
param = _params;
}
} else {
// will be object, so declare it one
param = {};
// loop over paramenters
for (const [key] of url.searchParams.entries()) {
// check if not yet set
if (typeof param[key] === 'undefined') {
// get the parameters multiple
let _params = url.searchParams.getAll(key);
// if 1 set as string, else attach array as is
param[key] = _params.length < 2 || single === true ?
_params[0] :
_params;
}
}
}
return param;
}
// __EMD__

View File

@@ -0,0 +1,34 @@
/*
Description: Translation call
Date: 2025//3/6
Creator: Clemens Schwaighofer
*/
export { l10nTranslation };
import { isObject } from './JavaScriptHelpers.mjs';
class l10nTranslation {
#i18n = {};
constructor(i18n) {
this.#i18n = i18n;
}
/**
* uses the i18n object created in the translation template
* that is filled from gettext in PHP
* @param {String} string text to translate
* @return {String} translated text (based on PHP selected language)
*/
__(string)
{
if (typeof this.#i18n !== 'undefined' && isObject(this.#i18n) && this.#i18n[string]) {
return this.#i18n[string];
} else {
return string;
}
}
}
// __END__

1238
src/utilsAll.mjs Normal file

File diff suppressed because it is too large Load Diff