/* $ 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.registerShadow = (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) { this._storeListener("click", func); 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; };