/* NAVIGATION */ let oldPushState = history.pushState; history.pushState = function pushState() { let ret = oldPushState.apply(this, arguments); window.dispatchEvent(new Event('pushstate')); window.dispatchEvent(new Event('locationchange')); return ret; }; window.addEventListener('popstate', () => { window.dispatchEvent(new Event('locationchange')); }); window.addEventListener('locationchange', locationChange); let urlBeforeChange = window.location.href; window.navigateTo = function(url) { window.history.pushState({}, '', url); } Object.defineProperty(window, 'routes', { configurable: true, enumerable: true, set: function(newValue) { Object.defineProperty(window, 'routes', { value: newValue, writable: false, configurable: false, enumerable: true }); locationChange(); }, get: function() { return window.routes; } }); function locationChange() { let URL = window.location.pathname.split("/").filter(d => (d !== 'Web') && (!d.includes('.html'))).join("/") if(URL === "") URL = "/" console.log("Location change: ", URL) if(!window.routes[URL]) { console.error("Quill: no URL for this route: ", URL) return } let page = new window.routes[URL]() window.rendering.push(page) page.render() window.rendering.pop(page) urlBeforeChange = window.location.href; } /* $() */ HTMLElement.prototype.$ = function(selector) { return window.$(selector, this) } DocumentFragment.prototype.$ = function(selector) { return window.$(selector, this) } window.$ = function(selector, el = document) { if(selector[0] === "#" || selector.includes("[name")) { return el.querySelector(selector) } else { return 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);"); } /* STRING TRANSLATORS */ window.css = function css(cssString) { let container = document.querySelector("style#quillStyles"); if(!container) { container = document.createElement('style'); container.id = "quillStyles"; 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(htmlString) { let container = document.createElement('div'); container.innerHTML = htmlString; // If there's only one child, return it directly if (container.children.length === 1) { return container.children[0]; } // If there are multiple children, use a DocumentFragment let fragment = document.createDocumentFragment(); while (container.firstChild) { fragment.appendChild(container.firstChild); } return fragment; }; /* COMPATIBILITY */ function detectMobile() { const mobileDeviceRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i; return mobileDeviceRegex.test(navigator.userAgent); } function getSafariVersion() { const userAgent = navigator.userAgent; const isSafari = userAgent.includes("Safari") && !userAgent.includes("Chrome"); if (isSafari) { const safariVersionMatch = userAgent.match(/Version\/(\d+\.\d+)/); const safariVersion = safariVersionMatch ? parseFloat(safariVersionMatch[1]) : null; return safariVersion; } } /* REGISTER */ class ObservedArray extends Array { parent; name; constructor(arr = [], parent, name) { super(); this.parent = parent this.name = name this.push(...arr); } triggerParent() { this.parent[this.name] = this } push(...args) { const result = super.push(...args); this.triggerParent() return result; } pop() { const result = super.pop(); this.triggerParent() return result; } shift() { const result = super.shift(); this.triggerParent() return result; } unshift(...args) { const result = super.unshift(...args); this.triggerParent() return result; } splice(start, deleteCount, ...items) { const removedItems = super.splice(start, deleteCount, ...items); if (items.length > 0) { console.log(`Inserted ${items.length} items:`, items); } if (removedItems.length > 0) { console.log(`Removed ${removedItems.length} items:`, removedItems); } this.triggerParent() return removedItems; } } class ObservedObject { constructor() { this._observers = {} } static decode(obj) { let instance = new this() Object.keys(instance).forEach((key) => { if(key[0] === "$") { key = key.slice(1) instance._observers[key] = new Map() const backingFieldName = `_${key}`; Object.defineProperty(instance, key, { set: function(newValue) { if(Array.isArray(newValue) && newValue.parent === undefined) { instance[backingFieldName] = new ObservedArray(newValue, this, key) } else { instance[backingFieldName] = newValue; } for (let [observer, properties] of instance._observers[key]) { for (let property of properties) { if(property === "children") { Registry.rerender(observer) } else { observer[property] = newValue; } } } }, get: function() { Registry.lastState.push(key) Registry.lastState.push(instance[backingFieldName]) return instance[backingFieldName]; }, enumerable: true, configurable: true }); delete instance["$" + key] } if(obj[key]) { instance[key] = obj[key] } else { if(!instance[key]) { throw new Error(`ObservedObject: Non-default value "${key}" must be initialized!`) } } }) return instance } } window.Page = class Page { appendChild(el) { document.body.appendChild(el) } } window.Shadow = class Shadow extends HTMLElement { constructor() { super() } } window.Registry = class Registry { static initReactivity(elem, name, value) { let parent = window.rendering.last() if(Registry.lastState.length === 3) { let [objName, objField, fieldValue] = Registry.lastState if(!objName) return; let valueCheck = parent[objName][objField] if(valueCheck && valueCheck === value) { if(!parent[objName]._observers[objField].get(elem)) { parent[objName]._observers[objField].set(elem, []) } let properties = parent[objName]._observers[objField].get(elem) if(!properties.includes(name)) { properties.push(name) } } } else { let [stateUsed, stateValue] = Registry.lastState if(!stateUsed) return; if(stateUsed && parent[stateUsed] === value) { if(!parent._observers[stateUsed].get(elem)) { parent._observers[stateUsed].set(elem, []) } parent._observers[stateUsed].get(elem).push(name) } } Registry.lastState = [] } static render = (el, parent) => { let renderParent = window.rendering[window.rendering.length-1] if(renderParent) { renderParent.appendChild(el) } window.rendering.push(el) el.render() window.rendering.pop(el) } static rerender = (el) => { if(el.parentElement) { window.rendering.push(el.parentElement) } window.rendering.push(el) el.innerHTML = "" el.render() window.rendering.pop() window.rendering.pop() } static testInitialized(el) { let fields = Object.keys(el).filter(key => typeof el[key] !== 'function' && key !== "_observers" ) for(let field of fields) { if(el[field] === undefined) { throw new Error(`Quill: field "${field}" must be initialized`) } } } static construct = (elem) => { // After default params are set, but before body of constructor const params = window.Registry.currentParams const allNames = Object.keys(elem).filter(key => typeof elem[key] !== 'function') const stateNames = allNames.filter(field => /^[$][^$]/.test(field)).map(str => str.substring(1)); const observedObjectNames = allNames.filter(field => /^[$][$][^$]/.test(field)).map(str => str.substring(2)); /* State Reactivity [stateName].get(elem).push(attribute) _observers = { name: Map(

: [innerText, background] ), } OO Reactivity: [objectName][objectField].get(elem).push(attribute) $$form: extends ObservedObject { _observers = { canvasPosition: Map(

: [position] ), path: Map(

: [innerText] ) } } */ function makeState(elem, stateNames, params) { elem._observers = {} // State -> Attributes: set each state value as getter and setter stateNames.forEach(name => { const backingFieldName = `_${name}`; elem._observers[name] = new Map() Object.defineProperty(elem, name, { set: function(newValue) { // console.log(`Setting state ${name} to `, newValue); elem[backingFieldName] = newValue; // Use the backing field to store the value elem.setAttribute(name, typeof newValue === "object" ? "{..}" : newValue); for (let [observer, properties] of elem._observers[name]) { for (let property of properties) { observer[property] = newValue; } } }, get: function() { Registry.lastState = [name, elem[backingFieldName]] // check which elements are observing the return elem[backingFieldName]; // Provide a getter to access the backing field value }, enumerable: true, configurable: true }); if(elem["$" + name] !== undefined) { elem[name] = elem["$" + name] } delete elem["$" + name] }); } function makeObservedObjects(elem, objectNames, params) { objectNames.forEach(name => { const backingFieldName = `_${name}`; Object.defineProperty(elem, name, { set: function(newValue) { elem[backingFieldName] = newValue; }, get: function() { Registry.lastState = [name] return elem[backingFieldName]; }, enumerable: true, configurable: true }); if(elem["$$" + name] !== undefined) { elem[name] = elem["$$" + name] } delete elem["$$" + name] }); } makeState(elem, stateNames, params) makeObservedObjects(elem, observedObjectNames, params) let i = -1 for (let param of params) { i++ if(i > allNames.length) { console.error(`${el.prototype.constructor.name}: too many parameters for field!`) return } let bareName = allNames[i].replace(/^(\$\$|\$)/, ''); if(elem[bareName] === undefined) { if(allNames[i].startsWith("$$") && !(param instanceof ObservedObject)) { throw new Error(`Field ${allNames[i]} must be an Observed Object!`) } elem[bareName] = param } } } static register = (el, tagname) => { let stateVariables = this.parseClassFields(el).filter(field => /^[$][^$]/.test(field)).map(str => str.substring(1)); el = this.parseConstructor(el) // Observe attributes Object.defineProperty(el, 'observedAttributes', { get: function() { return stateVariables; } }); // Attributes -> State Object.defineProperty(el.prototype, 'attributeChangedCallback', { value: function(name, oldValue, newValue) { const fieldName = `${name}`; let blacklistedValues = ["[object Object]", "{..}", this[fieldName]] if (stateVariables.includes(fieldName) && !blacklistedValues.includes(newValue)) { this[fieldName] = newValue; } }, writable: true, configurable: true }); customElements.define(tagname, el) // Actual Constructor window[el.prototype.constructor.name] = function (...params) { window.Registry.currentParams = params let elIncarnate = new el(...params) Registry.render(elIncarnate) return elIncarnate } } static parseClassFields(classObject) { let str = classObject.toString(); const lines = str.split('\n'); const fields = []; let braceDepth = 0; // Tracks the depth of curly braces to identify when we're inside a function/method for (let line of lines) { const trimmedLine = line.trim(); // Update braceDepth based on the current line braceDepth += (trimmedLine.match(/{/g) || []).length; braceDepth -= (trimmedLine.match(/}/g) || []).length; // Check if the line is outside any function/method (top-level within the class) if (braceDepth === 1) { // Attempt to match a class field declaration with or without initialization const fieldMatch = trimmedLine.match(/^([a-zA-Z_$][0-9a-zA-Z_$]*)\s*(=|;|\n|$)/); if (fieldMatch) { fields.push(fieldMatch[1]); } } // If we encounter the constructor, stop the parsing as we're only interested in fields above it if (trimmedLine.startsWith('constructor')) { break; } } return fields; } static parseConstructor(classObject) { let str = classObject.toString(); const lines = str.split('\n'); let modifiedLines = []; let braceDepth = 0; let constructorFound = false let superCallFound = false; let constructorEndFound = false; for (let i = 0; i < lines.length; i++) { let line = lines[i]; const trimmedLine = line.trim(); modifiedLines.push(line); braceDepth += (trimmedLine.match(/{/g) || []).length; braceDepth -= (trimmedLine.match(/}/g) || []).length; if (trimmedLine.startsWith('constructor(')) { constructorFound = true; } if (constructorFound && trimmedLine.startsWith('super(') && !superCallFound) { superCallFound = true; modifiedLines.push(` window.Registry.construct(this, window.Registry.currentStateVariables, ...window.Registry.currentParams);`); } if (constructorFound && braceDepth === 1 && superCallFound && !constructorEndFound) { modifiedLines.splice(modifiedLines.length - 1, 0, ' Object.preventExtensions(this);'); modifiedLines.splice(modifiedLines.length - 1, 0, ' window.Registry.testInitialized(this);'); constructorEndFound = true } } if(superCallFound) { let modifiedStr = modifiedLines.join('\n'); return eval('(' + modifiedStr + ')'); } if(constructorFound) { throw new Error("Quill: Constructor must have super()! " + lines[0]) } else { let constructorString = ` constructor(...params) { super(...params) window.Registry.construct(this) } ` let closingBracket = str.lastIndexOf("}"); str = str.slice(0, closingBracket - 1) + constructorString + "\n}" return eval('(' + str + ')'); } } } /* DEFAULT WRAPPERS */ window.ForEach = function (arr, cb) { Registry.initReactivity(window.rendering.last(), "children", arr) arr.forEach((el, i) => { cb(el, i) }) } window.a = function a({ href, name=href } = {}) { let link = document.createElement("a") link.setAttribute('href', href); link.innerText = name Registry.render(link) return link } window.img = function img({width="", height="", src=""}) { let image = new Image() if(width) image.style.width = width if(height) image.style.height = height if(src) image.src = src Registry.render(image) return image } window.p = function p(innerText) { let para = document.createElement("p") para.innerText = innerText Registry.initReactivity(para, "innerText", innerText) Registry.render(para) return para } window.div = function (innerText) { let div = document.createElement("div") div.innerText = innerText Registry.render(div) return div } window.span = function (innerText) { let span = document.createElement("span") span.innerText = innerText Registry.render(span) return span } /* PROTOTYPE FUNCTIONS */ Array.prototype.last = function() { return this[this.length-1] } HTMLElement.prototype.addAttribute = function(name) { this.setAttribute(name, "") } HTMLElement.prototype.ownHTML = function() { return this.startingTag() + this.endingTag() } HTMLElement.prototype.startingTag = function() { const tag = this.tagName.toLowerCase(); let html = `<${tag}`; for (const attr of this.attributes) { html += ` ${attr.name}="${attr.value}"`; } html += `>`; return html; } HTMLElement.prototype.endingTag = function() { const tag = this.tagName.toLowerCase(); return ``; } HTMLElement.prototype.render = function (...els) { if(els.length > 0) { this.innerHTML = "" els.forEach((el) => { this.appendChild(el) }) } return this } HTMLElement.prototype.class = function(classNames) { this.className = classNames return this } /* PROTOTYPE STYLING */ HTMLElement.prototype.styleVar = function(name, value) { this.style.setProperty(name, value) return this } HTMLElement.prototype.color = function(value) { this.style.color = value return this } HTMLElement.prototype.background = function(value) { this.style.backgroundColor = value return this } HTMLElement.prototype.fontSize = function(value) { this.style.fontSize = value return this } HTMLElement.prototype.borderRadius = function(value) { this.style.borderRadius = value return this } HTMLElement.prototype.padding = function(direction, amount) { const directionName = `padding${direction.charAt(0).toUpperCase()}${direction.slice(1)}`; if (typeof amount === 'number') { this.style[directionName] = `${amount}px`; } else { this.style[directionName] = amount; } return this } HTMLElement.prototype.outline = function(value) { this.style.outline = value return this } HTMLElement.prototype.maxWidth = function(value) { this.style.maxWidth = value return this } HTMLElement.prototype.margin = function(direction, amount) { const directionName = `margin${direction.charAt(0).toUpperCase()}${direction.slice(1)}`; if (typeof amount === 'number') { this.style[directionName] = `${amount}px`; } else { this.style[directionName] = amount; } return this } HTMLElement.prototype.transform = function(value) { this.style.transform = value 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.position = function({x, y} = {}) { if(!x || !y) { console.error("HTMLElement.position: must have valid x and y values: {x: 12, y: 23} where x and y are percentages") return; } let computed = window.getComputedStyle(this).position if(!(computed === "absolute" || computed === "fixed")) { this.style.position = "absolute" } this.style.left = `${x}%` this.style.top = `${y}%` return this } HTMLElement.prototype.overflow = function(value) { if(!(value === "visible" || value === "hidden" || value === "clip" || value === "scroll" || value === "auto")) { console.error("HTMLElement.overlflow: must have valid overflow value!") return; } this.style.overflow = value; return this } /* PROTOTYPE EVENTS */ HTMLElement.prototype.onClick = function(func) { this.addEventListener("click", func) return this } window.register = Registry.register window.rendering = []