Files
Hyperia/ui/_/code/quill.js
2025-11-07 20:14:38 -06:00

838 lines
24 KiB
JavaScript

/*
Sam Russell
Captured Sun
11.7.25 - changed registerShadow() to register(), changed onClick() to be like onHover()
11.6.25 - adding default value for "button()" "children" parameter
10.29.25 - adding "gap()" and "label()" functions
*/
/* $ NAVIGATION */
let oldPushState = history.pushState;
history.pushState = function pushState() {
let ret = oldPushState.apply(this, arguments);
window.dispatchEvent(new Event('pushstate'));
window.dispatchEvent(new Event('navigate'));
return ret;
};
window.addEventListener('popstate', () => {
window.dispatchEvent(new Event('navigate'));
});
window.navigateTo = function(url) {
window.dispatchEvent(new Event('navigate'));
window.history.pushState({}, '', url);
}
/* $ SELECTOR */
HTMLElement.prototype.$ = function(selector) {
return window.$(selector, this)
}
DocumentFragment.prototype.$ = function(selector) {
return window.$(selector, this)
}
window.$ = function(selector, el = document) {
return el.querySelector(selector)
}
window.$$ = function(selector, el = document) {
return Array.from(el.querySelectorAll(selector))
}
/* CONSOLE */
console.red = function(message) {
this.log(`%c${message}`, "color: rgb(254, 79, 42);");
};
console.green = function(message) {
this.log(`%c${message}`, "color: rgb(79, 254, 42);");
}
/* GET CSS VARIABLES FOR DARK OR LIGHT MODE */
window.darkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.classList.add(darkMode ? 'dark' : 'light');
window.getColor = function(name) {
const rootStyles = getComputedStyle(document.documentElement);
const color = rootStyles.getPropertyValue(`--${name}`).trim();
if(!color) {
throw new Error("Color not found")
}
return color
}
/* MOBILE */
window.isMobile = function isMobile() {
return /Android|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i.test(navigator.userAgent);
}
window.css = function css(cssString) {
let container = document.querySelector("style#pageStyle");
if(!container) {
container = document.createElement('style');
container.id = "pageStyle";
document.head.appendChild(container);
}
let primarySelector = cssString.substring(0, cssString.indexOf("{")).trim();
primarySelector = primarySelector.replace(/\*/g, "all");
primarySelector = primarySelector.replace(/#/g, "id-");
primarySelector = primarySelector.replace(/,/g, "");
let stylesheet = container.querySelector(`:scope > style[id='${primarySelector}']`)
if(!stylesheet) {
stylesheet = document.createElement('style');
stylesheet.id = primarySelector;
stylesheet.appendChild(document.createTextNode(cssString));
container.appendChild(stylesheet);
} else {
stylesheet.innerText = cssString
}
}
window.html = function html(elementString) {
let parser = new DOMParser();
let doc = parser.parseFromString(elementString, 'text/html');
return doc.body.firstChild;
}
window.util = {}
window.util.observeClassChange = (el, callback) => {
if (!el || !(el instanceof Element)) {
throw new Error("observeClassChange requires a valid DOM element.");
}
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === "attributes" && mutation.attributeName === "class") {
callback(el.classList);
}
}
});
observer.observe(el, {
attributes: true,
attributeFilter: ["class"]
});
return observer; // Optional: return it so you can disconnect later
}
/* PAGE SETUP */
Object.defineProperty(Array.prototype, 'last', {
get() {
return this[this.length - 1];
},
enumerable: false,
});
/* QUILL */
window.quill = {
rendering: [],
render: (el) => {
if(el instanceof Shadow) {
let parent = quill.rendering[quill.rendering.length-1]
if(!parent) {
parent = document.body
}
parent.appendChild(el)
} else {
if(!el.render) {el.render = () => {}}
let parent = quill.rendering[quill.rendering.length-1]
if(!parent) throw new Error("Quill: no parent for element")
parent.appendChild(el)
}
quill.rendering.push(el)
el.render()
quill.rendering.pop(el)
},
rerender: (el) => {
Array.from(el.attributes).forEach(attr => el.removeAttribute(attr.name));
el.innerHTML = ""
el.removeAllListeners()
quill.rendering.push(el)
el.render()
quill.rendering.pop()
},
loadPage: () => {
let URL = window.location.pathname
if(!window.routes[URL]) {
throw new Error("No URL for this route: ", URL)
}
let pageClass = window[routes[URL]]
document.title = pageClass.title ?? document.title
document.body.innerHTML = ""
let page = new pageClass()
quill.render(page)
},
isStack: (el) => {
return el.classList.contains("HStack") || el.classList.contains("ZStack") || el.classList.contains("VStack")
},
}
window.Shadow = class extends HTMLElement {
constructor() {
super()
}
}
window.register = (el, tagname) => {
if (typeof el.prototype.render !== 'function') {
throw new Error("Element must have a render: " + el.prototype.constructor.name)
}
if(!tagname) {
tagname = el.prototype.constructor.name.toLowerCase() + "-"
}
customElements.define(tagname, el)
if(el.css) {
css(el.css)
}
window[el.prototype.constructor.name] = function (...params) {
let instance = new el(...params)
quill.render(instance)
return instance
}
}
HTMLElement.prototype.rerender = function() {
quill.rerender(this)
}
/* Styling */
window.vh = "vh"
window.vw = "vw"
window.px = "px"
window.em = "em"
window.rem = "rem"
window.inches = "in"
HTMLElement.prototype.addStyle = function(func) {
return func(this)
}
window.css = function css(cssString) {
let container = document.querySelector("style#pageStyle");
if(!container) {
container = document.createElement('style');
container.id = "pageStyle";
document.head.appendChild(container);
}
let primarySelector = cssString.substring(0, cssString.indexOf("{")).trim();
primarySelector = primarySelector.replace(/\*/g, "all");
primarySelector = primarySelector.replace(/#/g, "id-");
primarySelector = primarySelector.replace(/,/g, "");
let stylesheet = container.querySelector(`:scope > style[id='${primarySelector}']`)
if(!stylesheet) {
stylesheet = document.createElement('style');
stylesheet.id = primarySelector;
stylesheet.appendChild(document.createTextNode(cssString));
container.appendChild(stylesheet);
} else {
stylesheet.innerText = cssString
}
}
function extendHTMLElementWithStyleSetters() {
let allStyleProps = Object.keys(document.createElement("div").style)
allStyleProps.forEach(prop => {
if(prop === "translate") return
HTMLElement.prototype[prop] = function(value) {
this.style[prop] = value;
return this;
};
});
}
extendHTMLElementWithStyleSetters();
HTMLElement.prototype.padding = function(one, two, three = "px") {
const setPadding = (side, val) => {
const directionName = `padding${side.charAt(0).toUpperCase()}${side.slice(1)}`;
this.style[directionName] = (typeof val === 'number') ? `${val}${three}` : val;
};
if(one === "horizontal" || one === "vertical") { // is one a direction
if (one === "horizontal") {
setPadding("left", two);
setPadding("right", two);
} else if (one === "vertical") {
setPadding("top", two);
setPadding("bottom", two);
}
} else { // is two a value
if(typeof one !== 'number' || isNaN(one)) {
this.style.padding = one
} else {
this.style.padding = one + two
}
}
return this;
};
HTMLElement.prototype.paddingTop = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.paddingTop = value + unit
return this
}
HTMLElement.prototype.paddingLeft = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.paddingLeft = value + unit
return this
}
HTMLElement.prototype.paddingBottom = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.paddingBottom = value + unit
return this
}
HTMLElement.prototype.paddingRight = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.paddingRight = value + unit
return this
}
HTMLElement.prototype.margin = function(direction, value, unit = "px") {
if (!value) {
this.style.margin = direction;
return this;
}
const setMargin = (side, val) => {
const directionName = `margin${side.charAt(0).toUpperCase()}${side.slice(1)}`;
this.style[directionName] = (typeof val === 'number') ? `${val}${unit}` : val;
};
if (direction === "horizontal") {
setMargin("left", value);
setMargin("right", value);
} else if (direction === "vertical") {
setMargin("top", value);
setMargin("bottom", value);
} else {
setMargin(direction, value);
}
return this;
};
HTMLElement.prototype.marginTop = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.marginTop = value + unit
return this
}
HTMLElement.prototype.marginLeft = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.marginLeft = value + unit
return this
}
HTMLElement.prototype.marginBottom = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.marginBottom = value + unit
return this
}
HTMLElement.prototype.marginRight = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.marginRight = value + unit
return this
}
HTMLElement.prototype.width = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.width = value + unit
return this
}
HTMLElement.prototype.minWidth = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.minWidth = value + unit
return this
}
HTMLElement.prototype.height = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.height = value + unit
return this
}
HTMLElement.prototype.minHeight = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.minHeight = value + unit
return this
}
HTMLElement.prototype.fontSize = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
switch(value) {
case "6xl":
value = "3.75"; unit = "rem"
break;
case "5xl":
value = "3"; unit = "rem"
break;
case "4xl":
value = "2.25"; unit = "rem"
break;
case "3xl":
value = "1.875"; unit = "rem"
break;
case "2xl":
value = "1.5"; unit = "rem"
break;
case "xl":
value = "1.25"; unit = "rem"
break;
case "l":
value = "1.125"; unit = "rem"
break;
case "s":
value = "0.875"; unit = "rem"
break;
case "xs":
value = "0.75"; unit = "rem"
break;
default:
break;
}
this.style.fontSize = value + unit
return this
}
function checkPositionType(el) {
let computed = window.getComputedStyle(el).position
if(!(computed === "absolute" || computed === "fixed")) {
el.style.position = "absolute"
}
}
HTMLElement.prototype.x = function(value, unit = "px") {
if (typeof value !== 'number' || isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
checkPositionType(this)
this.style.left = value + unit
return this
}
HTMLElement.prototype.y = function(value, unit = "px") {
if (typeof value !== 'number' || isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
checkPositionType(this)
this.style.top = value + unit
return this
}
HTMLElement.prototype.xRight = function(value, unit = "px") {
if (typeof value !== 'number' || isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
checkPositionType(this)
this.style.right = value + unit
return this
}
HTMLElement.prototype.yBottom = function(value, unit = "px") {
if (typeof value !== 'number' || isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
checkPositionType(this)
this.style.bottom = value + unit
return this
}
HTMLElement.prototype.borderRadius = function(value, unit = "px") {
if (typeof value !== 'number' || isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.borderRadius = value + unit
return this
}
HTMLElement.prototype.positionType = function (value) {
if(!(value === "absolute" || value === "relative" || value === "static" || value === "fixed" || value === "sticky")) {
console.error("HTMLElement.overlflow: must have valid overflow value!")
return;
}
this.style.position = value
return this
}
HTMLElement.prototype.gap = function(value, unit = "px") {
if (typeof value !== 'number' || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.gap = value + unit
return this
}
/* Elements */
quill.setChildren = function(el, innerContent) {
if(typeof innerContent === "string") {
el.innerText = innerContent
} else if(typeof innerContent === "function") {
el.render = innerContent
} else {
throw new Error("Children of unknown type")
}
}
window.a = function a( href, inner=href ) {
if(!href) throw new Error("quill a: missing href argument. Function: a( href, inner=href )")
let link = document.createElement("a")
link.setAttribute('href', href);
quill.setChildren(link, inner)
quill.render(link)
return link
}
window.img = function img(src, width="", height="") {
let image = document.createElement("img")
if(!src || !(typeof src==="string")) {
throw new Error("img: missing first argument: src | String")
} else {
image.src = src
}
if(width && typeof width === "string") {
image.style.width = width
} else if(width) {
image.style.width = width + "px"
}
if(height && typeof height === "string") {
image.style.height = height
} else if(height) {
image.style.height = height + "px"
}
quill.render(image)
return image
}
HTMLImageElement.prototype.backgroundColor = function(value) {
if (this.src.endsWith('.svg') || this.src.startsWith('data:image/svg+xml')) {
fetch(this.src).then(response => response.text())
.then(svgText => {
const modifiedSvg = svgText.replace(/\bfill="[^"]*"/g, `fill="${value}"`);
const blob = new Blob([modifiedSvg], { type: 'image/svg+xml' });
this.src = URL.createObjectURL(blob);
}).catch(error => {
console.error('Error updating SVG fill:', error);
});
} else {
this.style.backgroundColor = value;
}
return this; // Always returns the element itself
};
window.p = function p(innerText) {
let el = document.createElement("p")
if(typeof innerText === "function") {
el.render = innerText
} else {
el.innerText = innerText
}
el.style.margin = "0";
quill.render(el)
return el
}
window.h1 = function h1(innerText) {
let el = document.createElement("h1")
el.innerText = innerText
quill.render(el)
return el
}
window.h2 = function h2(innerText) {
let el = document.createElement("h2")
el.innerText = innerText
quill.render(el)
return el
}
window.h3 = function h3(innerText) {
let el = document.createElement("h3")
el.innerText = innerText
quill.render(el)
return el
}
window.div = function (innerText) {
let el = document.createElement("div")
el.innerText = innerText ?? ""
quill.render(el)
return el
}
window.span = function (innerText) {
let el = document.createElement("span")
el.innerText = innerText
quill.render(el)
return el
}
window.button = function (children = "") {
let el = document.createElement("button")
quill.setChildren(el, children)
quill.render(el)
return el
}
window.form = function(cb) {
let el = document.createElement("form")
el.render = cb
quill.render(el)
return el
}
window.input = function(placeholder, width, height) {
let el = document.createElement("input")
el.placeholder = placeholder
el.style.width = width
el.style.height = height
quill.render(el)
return el
}
window.label = function(text) {
let el = document.createElement("label")
el.innerText = text
quill.render(el)
return el
}
window.textarea = function(placeholder) {
let el = document.createElement("textarea")
el.placeholder = placeholder
quill.render(el)
return el
}
/* STACKS */
window.VStack = function (cb = () => {}) {
let styles = `
display: flex;
flex-direction: column;
`
let nowRendering = quill.rendering[quill.rendering.length-1]
if (nowRendering.innerHTML.trim() === "" && !quill.isStack(nowRendering)) {
nowRendering.style.cssText += styles
nowRendering.classList.add("VStack")
cb()
return nowRendering
}
let div = document.createElement("div")
div.classList.add("VStack")
div.style.cssText += styles
div.render = cb
quill.render(div)
return div
}
window.HStack = function (cb = () => {}) {
let styles = `
display: flex;
flex-direction: row;
`;
let nowRendering = quill.rendering[quill.rendering.length - 1];
if (nowRendering.innerHTML.trim() === "" && !quill.isStack(nowRendering)) {
nowRendering.style.cssText += styles;
nowRendering.classList.add("HStack")
cb();
return nowRendering;
}
let div = document.createElement("div");
div.classList.add("HStack");
div.style.cssText += styles;
div.render = cb;
quill.render(div);
return div;
};
window.ZStack = function (cb = () => {}) {
let nowRendering = quill.rendering[quill.rendering.length - 1];
if (nowRendering.innerHTML.trim() === "" && !quill.isStack(nowRendering)) {
nowRendering.classList.add("ZStack")
cb();
return nowRendering;
}
let div = document.createElement("div");
div.classList.add("ZStack");
div.render = cb;
quill.render(div);
return div;
};
/* EVENTS */
HTMLElement.prototype.onAppear = function(func) {
func(this);
return this;
};
HTMLElement.prototype.onClick = function(func) {
const onMouseDown = () => func.call(this, true);
const onMouseUp = () => func.call(this, false);
this._storeListener("mousedown", onMouseDown);
this._storeListener("mouseup", onMouseUp);
return this;
};
HTMLElement.prototype.onMouseDown = function(func) {
this._storeListener("mousedown", func);
return this;
};
HTMLElement.prototype.onMouseUp = function(func) {
this._storeListener("mouseup", func);
return this;
};
HTMLElement.prototype.onRightClick = function(func) {
this._storeListener("contextmenu", func);
return this;
};
HTMLElement.prototype.onHover = function(cb) {
const onEnter = () => cb.call(this, true);
const onLeave = () => cb.call(this, false);
this._storeListener("mouseover", onEnter);
this._storeListener("mouseleave", onLeave);
return this;
};
HTMLElement.prototype.onFocus = function(cb) {
if (!this.matches('input, textarea, select, button')) {
throw new Error("Can't put focus event on non-form element!");
}
this._storeListener("focus", cb);
return this;
};
HTMLElement.prototype.onBlur = function(cb) {
if (!this.matches('input, textarea, select, button')) {
throw new Error("Can't put blur event on non-form element!");
}
this._storeListener("blur", cb);
return this;
};
HTMLElement.prototype.onKeyDown = function(cb) {
this._storeListener("keydown", cb);
return this;
};
/* QUIRK 1:
In all the other callback functions, the user can choose the scope of "this". It can be either the parent shadow or the element itself.
This listener only allows for the latter functionality. This is because the navigate event fires on the window.
Without binding, "this" would refer only to the window. So here we are compromising on one of the two.
*/
HTMLElement.prototype.onNavigate = function(cb) {
window._storeListener(window, "navigate", cb.bind(this));
return this;
};
HTMLElement.prototype.onEvent = function(name, cb) {
window._storeListener(window, name, cb);
return this;
};
HTMLElement.prototype._storeListener = function(type, handler, options) {
window._storeListener(this, type, handler, options)
}
window.__listeners = []
function _storeListener(target, type, handler, options) {
if (!target.__listeners) target.__listeners = [];
const optionsString = JSON.stringify(options);
const index = target.__listeners.findIndex(listener =>
listener.type === type &&
listener.handler.toString() === handler.toString() &&
JSON.stringify(listener.options) === optionsString
);
if (index === -1) { // Listener is new
target.addEventListener(type, handler, options);
target.__listeners.push({ type, handler, options });
} else { // Listener is a duplicate, can be replaced
const old = target.__listeners[index];
target.removeEventListener(old.type, old.handler, old.options);
// Replace with the new one
target.addEventListener(type, handler, options);
target.__listeners[index] = { type, handler, options };
}
}
HTMLElement.prototype.removeAllListeners = function() {
if (!this.__listeners) return;
for (const { type, handler, options } of this.__listeners) {
this.removeEventListener(type, handler, options);
}
this.__listeners = [];
return this;
};
/* ATTRIBUTES */
HTMLElement.prototype.attr = function(attributes) {
if (
typeof attributes !== "object" ||
attributes === null ||
Array.isArray(attributes)
) {
throw new TypeError("attr() expects an object with key-value pairs");
}
for (const [key, value] of Object.entries(attributes)) {
this.setAttribute(key, value);
}
return this;
};