Commit pirate JS files
This commit is contained in:
664
js/DomWidgets.js
Normal file
664
js/DomWidgets.js
Normal file
@@ -0,0 +1,664 @@
|
||||
/*
|
||||
Copyright (c) 2023-forever Douglas Malnati. All rights reserved.
|
||||
|
||||
See the /faq/tos page for details.
|
||||
|
||||
(If this generated header is stamped on a file which is a 3rd party file or under a different license or copyright, then ignore this copyright statement and use that file's terms.)
|
||||
*/
|
||||
|
||||
function GetViewableWidthAccountingForScrollbar()
|
||||
{
|
||||
// Check if a vertical scrollbar is present
|
||||
const isVerticalScrollbarPresent = document.documentElement.scrollHeight > window.innerHeight;
|
||||
|
||||
// If no vertical scrollbar, return the innerWidth as is
|
||||
if (!isVerticalScrollbarPresent) {
|
||||
return window.innerWidth;
|
||||
}
|
||||
|
||||
// Create a temporary element to measure the scrollbar width
|
||||
const div = document.createElement('div');
|
||||
div.style.visibility = 'hidden'; // Make sure it's not visible
|
||||
div.style.position = 'absolute';
|
||||
div.style.width = '100px'; // Set a fixed width for the element
|
||||
div.style.overflow = 'scroll'; // Enable scroll to ensure a scrollbar appears
|
||||
document.body.appendChild(div);
|
||||
|
||||
// Calculate the scrollbar width
|
||||
const scrollbarWidth = div.offsetWidth - div.clientWidth;
|
||||
|
||||
// Clean up the temporary div
|
||||
document.body.removeChild(div);
|
||||
|
||||
// Return the viewport width excluding the scrollbar
|
||||
return window.innerWidth - scrollbarWidth;
|
||||
}
|
||||
|
||||
function GetViewableHeightAccountingForScrollbar()
|
||||
{
|
||||
// Check if a horizontal scrollbar is present
|
||||
const isHorizontalScrollbarPresent = document.documentElement.scrollWidth > window.innerWidth;
|
||||
|
||||
// If no horizontal scrollbar, return the innerHeight as is
|
||||
if (!isHorizontalScrollbarPresent) {
|
||||
return window.innerHeight;
|
||||
}
|
||||
|
||||
// Create a temporary element to measure the scrollbar height
|
||||
const div = document.createElement('div');
|
||||
div.style.visibility = 'hidden'; // Make sure it's not visible
|
||||
div.style.position = 'absolute';
|
||||
div.style.height = '100px'; // Set a fixed height for the element
|
||||
div.style.overflow = 'scroll'; // Enable scroll to ensure a scrollbar appears
|
||||
document.body.appendChild(div);
|
||||
|
||||
// Calculate the scrollbar height
|
||||
const scrollbarHeight = div.offsetHeight - div.clientHeight;
|
||||
|
||||
// Clean up the temporary div
|
||||
document.body.removeChild(div);
|
||||
|
||||
// Return the viewport height excluding the scrollbar
|
||||
return window.innerHeight - scrollbarHeight;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class ZIndexHelper
|
||||
{
|
||||
static BASE_Z_INDEX = 1000;
|
||||
|
||||
constructor()
|
||||
{
|
||||
this.objDataList = [];
|
||||
}
|
||||
|
||||
// objects register to have a given property set to the zIndex to make them
|
||||
// the top-most at this time, and later in the future
|
||||
RegisterForTop(obj, prop)
|
||||
{
|
||||
this.objDataList.push({
|
||||
obj,
|
||||
prop,
|
||||
});
|
||||
|
||||
this.#AnnounceAll();
|
||||
|
||||
return this.objDataList.length;
|
||||
}
|
||||
|
||||
// request immediate top level
|
||||
RequestTop(obj)
|
||||
{
|
||||
// find its current location
|
||||
let idxFound = -1;
|
||||
for (let zIndex = 0; zIndex < this.objDataList.length; ++zIndex)
|
||||
{
|
||||
let objData = this.objDataList[zIndex];
|
||||
|
||||
if (objData.obj == obj)
|
||||
{
|
||||
idxFound = zIndex;
|
||||
}
|
||||
}
|
||||
|
||||
if (idxFound != -1)
|
||||
{
|
||||
// hold temporarily
|
||||
let objData = this.objDataList[idxFound];
|
||||
|
||||
// delete its location, effectively compacting list
|
||||
this.objDataList.splice(idxFound, 1);
|
||||
|
||||
// re-insert
|
||||
this.objDataList.push(objData);
|
||||
|
||||
// announce re-index
|
||||
this.#AnnounceAll();
|
||||
}
|
||||
}
|
||||
|
||||
#AnnounceAll()
|
||||
{
|
||||
for (let zIndex = 0; zIndex < this.objDataList.length; ++zIndex)
|
||||
{
|
||||
let objData = this.objDataList[zIndex];
|
||||
|
||||
objData.obj[objData.prop] = ZIndexHelper.BASE_Z_INDEX + zIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class DialogBox
|
||||
{
|
||||
static #zIndexHelper = new ZIndexHelper();
|
||||
static #instanceList = [];
|
||||
static #escapeHandlerSet = false;
|
||||
|
||||
constructor()
|
||||
{
|
||||
this.isDragging = false;
|
||||
this.offsetX = 0;
|
||||
this.offsetY = 0;
|
||||
|
||||
this.ui = this.#MakeUI();
|
||||
|
||||
DialogBox.#instanceList.push(this);
|
||||
DialogBox.#EnsureEscapeHandler();
|
||||
}
|
||||
|
||||
GetUI()
|
||||
{
|
||||
return this.ui;
|
||||
}
|
||||
|
||||
SetTitleBar(title)
|
||||
{
|
||||
this.titleBar.innerHTML = title;
|
||||
}
|
||||
|
||||
GetContentContainer()
|
||||
{
|
||||
return this.frameBody;
|
||||
}
|
||||
|
||||
ToggleShowHide()
|
||||
{
|
||||
if (this.floatingWindow.style.display === 'none')
|
||||
{
|
||||
this.Show();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Hide();
|
||||
}
|
||||
}
|
||||
|
||||
Show()
|
||||
{
|
||||
const STEP_SIZE_PIXELS = 50;
|
||||
|
||||
let zIndex = DialogBox.#zIndexHelper.RegisterForTop(this.floatingWindow.style, "zIndex");
|
||||
|
||||
if (this.floatingWindow.style.top == "50px" &&
|
||||
this.floatingWindow.style.left == "50px")
|
||||
{
|
||||
this.floatingWindow.style.top = `${STEP_SIZE_PIXELS * zIndex}px`;
|
||||
this.floatingWindow.style.left = `${STEP_SIZE_PIXELS * zIndex}px`;
|
||||
}
|
||||
|
||||
this.floatingWindow.style.display = 'flex';
|
||||
}
|
||||
|
||||
Hide()
|
||||
{
|
||||
DialogBox.#zIndexHelper.RequestTop(this.floatingWindow.style);
|
||||
|
||||
this.floatingWindow.style.display = 'none';
|
||||
}
|
||||
|
||||
static #EnsureEscapeHandler()
|
||||
{
|
||||
if (DialogBox.#escapeHandlerSet)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DialogBox.#escapeHandlerSet = true;
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key !== "Escape")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let topMost = null;
|
||||
let topZ = Number.NEGATIVE_INFINITY;
|
||||
for (const dlg of DialogBox.#instanceList)
|
||||
{
|
||||
if (!dlg || !dlg.floatingWindow)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dlg.floatingWindow.style.display === 'none')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let z = parseInt(dlg.floatingWindow.style.zIndex || "0");
|
||||
if (isNaN(z))
|
||||
{
|
||||
z = 0;
|
||||
}
|
||||
|
||||
if (z >= topZ)
|
||||
{
|
||||
topZ = z;
|
||||
topMost = dlg;
|
||||
}
|
||||
}
|
||||
|
||||
if (topMost)
|
||||
{
|
||||
topMost.Hide();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#MakeFloatingWindowFrame()
|
||||
{
|
||||
this.floatingWindow = document.createElement('div');
|
||||
|
||||
this.floatingWindow.style.boxSizing = "border-box";
|
||||
|
||||
this.floatingWindow.style.position = 'fixed';
|
||||
this.floatingWindow.style.top = '50px';
|
||||
this.floatingWindow.style.left = '50px';
|
||||
|
||||
this.floatingWindow.style.backgroundColor = '#f0f0f0';
|
||||
|
||||
this.floatingWindow.style.border = '1px solid black';
|
||||
this.floatingWindow.style.borderRadius = '5px';
|
||||
|
||||
this.floatingWindow.style.boxShadow = '2px 2px 8px black';
|
||||
|
||||
this.floatingWindow.style.padding = '0px';
|
||||
|
||||
this.floatingWindow.style.display = 'none'; // Initially hidden
|
||||
this.floatingWindow.style.zIndex = 1;
|
||||
|
||||
this.floatingWindow.style.flexDirection = "column";
|
||||
|
||||
|
||||
return this.floatingWindow;
|
||||
}
|
||||
|
||||
#MakeTopRow()
|
||||
{
|
||||
// create top row
|
||||
this.topRow = document.createElement('div');
|
||||
this.topRow.style.boxSizing = "border-box";
|
||||
|
||||
this.topRow.style.borderBottom = "1px solid black";
|
||||
this.topRow.style.borderTopRightRadius = "5px";
|
||||
this.topRow.style.borderTopLeftRadius = "5px";
|
||||
this.topRow.style.display = "flex";
|
||||
this.topRow.style.backgroundColor = "#ff323254";
|
||||
|
||||
// top row - title bar
|
||||
this.titleBar = document.createElement('div');
|
||||
this.titleBar.style.boxSizing = "border-box";
|
||||
|
||||
this.titleBar.style.flexGrow = "1";
|
||||
this.titleBar.style.borderRight = "1px solid black";
|
||||
this.titleBar.style.borderTopLeftRadius = "5px";
|
||||
|
||||
this.titleBar.style.padding = "3px";
|
||||
this.titleBar.style.backgroundColor = 'rgb(255, 255, 200)';
|
||||
this.titleBar.style.cursor = 'move'; // Indicate draggable behavior
|
||||
this.titleBar.innerHTML = "Dialog Box";
|
||||
this.topRow.appendChild(this.titleBar);
|
||||
|
||||
// top row - close button
|
||||
const closeButton = document.createElement('button');
|
||||
closeButton.textContent = 'X';
|
||||
// closeButton.style.cursor = 'pointer';
|
||||
closeButton.style.border = 'none';
|
||||
closeButton.style.backgroundColor = 'rgba(0,0,0,0)'; // transparent
|
||||
|
||||
this.topRow.appendChild(closeButton);
|
||||
|
||||
// Close button event handling
|
||||
closeButton.addEventListener('click', () => {
|
||||
this.Hide();
|
||||
});
|
||||
|
||||
return this.topRow;
|
||||
}
|
||||
|
||||
#MakeBody()
|
||||
{
|
||||
let dom = document.createElement('div');
|
||||
dom.style.boxSizing = "border-box";
|
||||
dom.style.padding = "3px";
|
||||
dom.style.width = "100%";
|
||||
dom.style.flexGrow = "1";
|
||||
dom.style.backgroundColor = "rgb(210, 210, 210)";
|
||||
|
||||
// only show scrollbars if necessary
|
||||
// (eg someone manually resizes dialog smaller than content minimum size)
|
||||
dom.style.overflowX = "auto";
|
||||
dom.style.overflowY = "auto";
|
||||
dom.style.scrollbarGutter = "stable";
|
||||
|
||||
// don't scroll the page, just the div
|
||||
let ScrollJustThis = dom => {
|
||||
dom.addEventListener('wheel', (e) => {
|
||||
const hasVerticalScrollbar = dom.scrollHeight > dom.clientHeight;
|
||||
|
||||
if (hasVerticalScrollbar)
|
||||
{
|
||||
e.stopPropagation();
|
||||
}
|
||||
else
|
||||
{
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// ScrollJustThis(dom)
|
||||
|
||||
return dom;
|
||||
}
|
||||
|
||||
#EnableDrag()
|
||||
{
|
||||
this.floatingWindow.addEventListener('mousedown', (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
DialogBox.#zIndexHelper.RequestTop(this.floatingWindow.style);
|
||||
});
|
||||
|
||||
this.titleBar.addEventListener('mousedown', (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
DialogBox.#zIndexHelper.RequestTop(this.floatingWindow.style);
|
||||
|
||||
this.isDragging = true;
|
||||
this.offsetX = e.clientX - this.floatingWindow.getBoundingClientRect().left;
|
||||
this.offsetY = e.clientY - this.floatingWindow.getBoundingClientRect().top;
|
||||
document.body.style.userSelect = 'none'; // Prevent text selection during drag
|
||||
});
|
||||
|
||||
// Drag the window
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (this.isDragging) {
|
||||
// determine viewable area
|
||||
let viewableWidth = GetViewableWidthAccountingForScrollbar();
|
||||
let viewableHeight = GetViewableHeightAccountingForScrollbar();
|
||||
|
||||
// prevent mouse from dragging popup outside the viewable
|
||||
// area on the left, right, and bottom.
|
||||
let cursorX = e.clientX;
|
||||
if (cursorX < 0) { cursorX = 0; }
|
||||
if (cursorX > viewableWidth) { cursorX = viewableWidth; }
|
||||
let cursorY = e.clientY;
|
||||
if (cursorY > viewableHeight) { cursorY = viewableHeight; }
|
||||
|
||||
// don't let the dialog go above the window at all
|
||||
let top = cursorY - this.offsetY;
|
||||
let left = cursorX - this.offsetX;
|
||||
if (top < 0) { top = 0; }
|
||||
|
||||
// apply
|
||||
this.floatingWindow.style.top = `${top}px`;
|
||||
this.floatingWindow.style.left = `${left}px`;
|
||||
}
|
||||
});
|
||||
|
||||
// Stop dragging
|
||||
document.addEventListener('mouseup', () => {
|
||||
if (this.isDragging) {
|
||||
this.isDragging = false;
|
||||
document.body.style.userSelect = ''; // Re-enable text selection
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#MakeUI()
|
||||
{
|
||||
let frame = this.#MakeFloatingWindowFrame();
|
||||
let frameTopRow = this.#MakeTopRow();
|
||||
this.frameBody = this.#MakeBody();
|
||||
|
||||
this.frameBody.marginTop = "2px";
|
||||
|
||||
frame.appendChild(frameTopRow);
|
||||
frame.appendChild(this.frameBody);
|
||||
|
||||
// don't let the page scroll when you hover the popup
|
||||
// (scrollable content section handled separately)
|
||||
// frame.addEventListener('wheel', (e) => {
|
||||
// e.preventDefault();
|
||||
// });
|
||||
|
||||
this.#EnableDrag();
|
||||
|
||||
return this.floatingWindow;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class CollapsableTitleBox
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
this.ui = this.#MakeUI();
|
||||
this.#SetUpEvents();
|
||||
}
|
||||
|
||||
GetUI()
|
||||
{
|
||||
return this.ui;
|
||||
}
|
||||
|
||||
SetTitle(title)
|
||||
{
|
||||
this.titleBar.innerHTML = title;
|
||||
}
|
||||
|
||||
GetContentContainer()
|
||||
{
|
||||
return this.box;
|
||||
}
|
||||
|
||||
SetMinWidth(minWidth)
|
||||
{
|
||||
this.ui.style.minWidth = minWidth;
|
||||
}
|
||||
|
||||
ToggleShowHide()
|
||||
{
|
||||
if (this.box.style.display === 'none')
|
||||
{
|
||||
this.Show();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Hide();
|
||||
}
|
||||
}
|
||||
|
||||
Show()
|
||||
{
|
||||
this.box.style.display = 'flex';
|
||||
}
|
||||
|
||||
Hide()
|
||||
{
|
||||
this.box.style.display = 'none';
|
||||
}
|
||||
|
||||
#SetUpEvents()
|
||||
{
|
||||
this.titleBar.addEventListener('click', () => {
|
||||
this.ToggleShowHide();
|
||||
});
|
||||
}
|
||||
|
||||
#MakeUI()
|
||||
{
|
||||
// entire structure
|
||||
this.ui = document.createElement('div');
|
||||
this.ui.style.boxSizing = "border-box";
|
||||
this.ui.style.backgroundColor = "white";
|
||||
this.ui.style.border = "1px solid grey";
|
||||
|
||||
// user reads this, click to hide/unhide
|
||||
this.titleBar = document.createElement('div');
|
||||
this.titleBar.style.boxSizing = "border-box";
|
||||
this.titleBar.style.padding = "3px";
|
||||
this.titleBar.style.backgroundColor = "rgb(240, 240, 240)";
|
||||
// this.titleBar.style.backgroundColor = "rgb(200, 200, 255)";
|
||||
this.titleBar.style.userSelect = "none";
|
||||
this.titleBar.style.cursor = "pointer";
|
||||
this.titleBar.innerHTML = "Title Bar";
|
||||
|
||||
// user content goes here
|
||||
this.box = document.createElement('div');
|
||||
this.box.style.boxSizing = "border-box";
|
||||
this.box.style.padding = "5px";
|
||||
this.box.style.boxShadow = "1px 1px 5px #555 inset";
|
||||
this.box.style.overflowX = "auto";
|
||||
this.box.style.overflowY = "auto";
|
||||
this.box.style.display = 'none'; // initially hidden
|
||||
|
||||
// pack
|
||||
this.ui.appendChild(this.titleBar);
|
||||
this.ui.appendChild(this.box);
|
||||
|
||||
return this.ui;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class RadioCheckbox
|
||||
{
|
||||
constructor(name)
|
||||
{
|
||||
this.name = name;
|
||||
this.ui = this.#MakeUI();
|
||||
|
||||
this.inputList = [];
|
||||
|
||||
this.fnOnChange = (val) => {};
|
||||
}
|
||||
|
||||
AddOption(labelText, value, checked)
|
||||
{
|
||||
// create input
|
||||
let input = document.createElement('input');
|
||||
input.type = "radio";
|
||||
input.name = this.name;
|
||||
input.value = value;
|
||||
if (checked)
|
||||
{
|
||||
input.checked = true;
|
||||
}
|
||||
this.inputList.push(input);
|
||||
|
||||
// set up label
|
||||
let label = document.createElement('label');
|
||||
label.appendChild(input);
|
||||
label.appendChild(document.createTextNode(` ${labelText}`));
|
||||
|
||||
// add to container
|
||||
if (this.inputList.length != 1)
|
||||
{
|
||||
this.ui.appendChild(document.createTextNode(' '));
|
||||
}
|
||||
this.ui.appendChild(label);
|
||||
|
||||
// set up events
|
||||
input.addEventListener('change', (e) => {
|
||||
this.fnOnChange(e.target.value);
|
||||
});
|
||||
}
|
||||
|
||||
SetOnChangeCallback(fn)
|
||||
{
|
||||
this.fnOnChange = fn;
|
||||
}
|
||||
|
||||
Trigger()
|
||||
{
|
||||
for (let input of this.inputList)
|
||||
{
|
||||
if (input.checked)
|
||||
{
|
||||
this.fnOnChange(input.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GetUI()
|
||||
{
|
||||
return this.ui;
|
||||
}
|
||||
|
||||
#MakeUI()
|
||||
{
|
||||
let ui = document.createElement('span');
|
||||
|
||||
return ui;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// write through and read-through cache stored persistently
|
||||
export class RadioCheckboxPersistent
|
||||
extends RadioCheckbox
|
||||
{
|
||||
constructor(name)
|
||||
{
|
||||
super(name);
|
||||
|
||||
this.val = null;
|
||||
|
||||
// cache currently-stored value
|
||||
if (localStorage.getItem(this.name) != null)
|
||||
{
|
||||
this.val = localStorage.getItem(this.name);
|
||||
}
|
||||
}
|
||||
|
||||
// add option except checked is just a suggestion.
|
||||
// if no prior value set, let suggestion take effect.
|
||||
// if prior value set, prior value rules.
|
||||
AddOption(labelText, value, checkedSuggestion)
|
||||
{
|
||||
let checked = checkedSuggestion;
|
||||
|
||||
if (this.val == null)
|
||||
{
|
||||
// let it happen
|
||||
}
|
||||
else
|
||||
{
|
||||
checked = this.val == value;
|
||||
}
|
||||
|
||||
super.AddOption(labelText, value, checked);
|
||||
|
||||
// cache and write through
|
||||
if (checked)
|
||||
{
|
||||
this.val = value;
|
||||
localStorage.setItem(this.name, this.val);
|
||||
}
|
||||
}
|
||||
|
||||
SetOnChangeCallback(fn)
|
||||
{
|
||||
super.SetOnChangeCallback((val) => {
|
||||
// capture the new value before passing back
|
||||
this.val = val;
|
||||
localStorage.setItem(this.name, this.val);
|
||||
|
||||
// callback
|
||||
fn(val);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user