diff --git a/index.js b/index.js index fcebb29..1c3c315 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,8 @@ /* Sam Russell Captured Sun + 12.17.25 - [Hyperia] - adding width, height functions. adding "e" to onClick. adding the non-window $$ funcs. + 12.16.25 - [comalyr] - State 11.25.25.1 - Added minHeight and minWidth to be counted as numerical styles 11.25.25 - Added onChange for check boxes, added setQuery / onQueryChanged for easy filtering 11.24.25 - Fixing onClick because it was reversed, adding event to onHover params @@ -68,6 +70,12 @@ window.$ = function(selector, el = document) { window.$$ = function(selector, el = document) { return Array.from(el.querySelectorAll(selector)) } +HTMLElement.prototype.$$ = function(selector) { + return window.$$(selector, this) +} +DocumentFragment.prototype.$$ = function(selector) { + return window.$$(selector, this) +} /* CONSOLE */ @@ -163,6 +171,7 @@ Object.defineProperty(Array.prototype, 'last', { window.quill = { rendering: [], + lastState: null, render: (el) => { if(el instanceof Shadow) { @@ -193,19 +202,6 @@ window.quill = { 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") }, @@ -216,6 +212,7 @@ window.Shadow = class extends HTMLElement { super() } } + window.register = (el, tagname) => { if (typeof el.prototype.render !== 'function') { throw new Error("Element must have a render: " + el.prototype.constructor.name) @@ -230,6 +227,42 @@ window.register = (el, tagname) => { window[el.prototype.constructor.name] = function (...params) { let instance = new el(...params) + if(instance.state) { + let proxy = new Proxy(instance.state, { + get(target, prop, receiver) { + if (typeof prop === "symbol") { // Ignore internal / symbol accesses + return Reflect.get(target, prop, receiver); + } + + quill.lastLastState = quill.lastState + quill.lastState = prop; + return Reflect.get(target, prop, receiver); + }, + set(target, prop, value, receiver) { + const oldValue = target[prop]; + if (oldValue === value) return true; + + const result = Reflect.set(target, prop, value, receiver); + instance.stateWatchers[prop].forEach((cb) => cb()) + return result; + } + }); + Object.defineProperty(instance, "state", { + value: proxy, + writable: false, + configurable: false, + enumerable: true + }); + + let stateWatchers = {} + Object.keys(instance.state).forEach((key) => stateWatchers[key] = []) + Object.defineProperty(instance, "stateWatchers", { + value: stateWatchers, + writable: false, + configurable: false, + enumerable: true + }); + } quill.render(instance) return instance } @@ -314,6 +347,7 @@ function extendHTMLElementWithStyleSetters() { case "marginRight": case "textUnderlineOffset": + case "letterSpacing": return "unit-number" @@ -333,24 +367,27 @@ function extendHTMLElementWithStyleSetters() { switch (type) { case "unit-number": - HTMLElement.prototype[prop] = function(value, unit = "px") { - if ((typeof value !== "number" || isNaN(value)) && value !== "auto") { - throw new Error(`Invalid value for ${prop}: ${value}. Expected a number.`); - } + HTMLElement.prototype[prop] = StyleFunction(function(value, unit = "px") { if(value === "auto") { this.style[prop] = value return this } this.style[prop] = value + unit; + if (value !== "" && this.style[prop] === "") { + throw new Error(`Invalid CSS value for ${prop}: ` + value + unit); + } return this; - }; + }); break; case "string": - HTMLElement.prototype[prop] = function(value) { + HTMLElement.prototype[prop] = StyleFunction(function(value) { this.style[prop] = value; + if (value !== "" && this.style[prop] === "") { + throw new Error(`Invalid CSS value for ${prop}: ` + value); + } return this; - }; + }); break; } }); @@ -358,46 +395,86 @@ function extendHTMLElementWithStyleSetters() { extendHTMLElementWithStyleSetters(); +HTMLElement.prototype.addStateWatcher = function(field, cb) { + let parent = this + while(!(parent instanceof Shadow)) { + parent = parent.parentNode + } + parent.stateWatchers[field].push(cb) +} + +// Currently only works for one state variable in the function +// Could probably be fixed by just making lastState an array and clearing it out every function call? +HTMLElement.prototype.setUpState = function(styleFunc, cb) { + let format = (value) => {return Array.isArray(value) ? value : [value]} + + // 1. Run the callback to get the style argument and also update lastState + let styleArgs = format(cb()) + + // 2. Check if lastState has really been updated. If not, the user-provided cb did not access valid state + if(!quill.lastState) { + throw new Error("Quill: style state function does not access valid state") + } + + // 3. Construct function to run when state changes + let onStateChange = () => { + styleFunc.call(this, ...format(cb())) + } + + // 4. Now listen for the state to change + this.addStateWatcher(quill.lastState, onStateChange) + + // 5. Run the original function again, this time with the actual arguments + quill.lastState = null + styleFunc.call(this, ...styleArgs) +} + +function StyleFunction(func) { + let styleFunction = function(value, unit = "px") { + if(typeof value === 'function') { + this.setUpState(styleFunction, value) + return this + } else { + func.call(this, value, unit); // ".call" ensures that "this" is correct + return this + } + } + + return styleFunction +} + HTMLElement.prototype.styles = function(cb) { cb.call(this, this) return this } -HTMLElement.prototype.paddingVertical = function(value, unit = "px") { - if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value)) - throw new Error(`Invalid value: ${value}. Expected a number.`); +/* Type 1 */ + +HTMLElement.prototype.paddingVertical = StyleFunction(function(value, unit = "px") { this.style.paddingTop = value + unit this.style.paddingBottom = value + unit return this -} +}) -HTMLElement.prototype.paddingHorizontal = function(value, unit = "px") { - if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value)) - throw new Error(`Invalid value: ${value}. Expected a number.`); +HTMLElement.prototype.paddingHorizontal = StyleFunction(function(value, unit = "px") { this.style.paddingRight = value + unit this.style.paddingLeft = value + unit return this -} +}) -HTMLElement.prototype.marginVertical = function(value, unit = "px") { - if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value)) - throw new Error(`Invalid value: ${value}. Expected a number.`); +HTMLElement.prototype.marginVertical = StyleFunction(function(value, unit = "px") { this.style.marginTop = value + unit this.style.marginBottom = value + unit return this -} +}) -HTMLElement.prototype.marginHorizontal = function(value, unit = "px") { - if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value)) - throw new Error(`Invalid value: ${value}. Expected a number.`); +HTMLElement.prototype.marginHorizontal = StyleFunction(function(value, unit = "px") { this.style.marginRight = value + unit this.style.marginLeft = 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.`); +HTMLElement.prototype.fontSize = StyleFunction(function(value, unit = "px") { switch(value) { case "6xl": @@ -441,7 +518,7 @@ HTMLElement.prototype.fontSize = function(value, unit = "px") { } this.style.fontSize = value + unit return this -} +}) function checkPositionType(el) { let computed = window.getComputedStyle(el).position @@ -450,6 +527,26 @@ function checkPositionType(el) { } } +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 + if(window.getComputedStyle(this).display === "inline") { + this.style.display = "block" + } + 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 + if(window.getComputedStyle(this).display === "inline") { + this.style.display = "block" + } + return this +} + HTMLElement.prototype.x = function(value, unit = "px") { if (typeof value !== 'number' || isNaN(value)) throw new Error(`Invalid value: ${value}. Expected a number.`); @@ -661,7 +758,7 @@ window.form = function(cb) { return el } -window.input = function(placeholder, width, height) { +window.input = function(placeholder = "", width, height) { let el = document.createElement("input") el.placeholder = placeholder el.style.width = width @@ -670,14 +767,18 @@ window.input = function(placeholder, width, height) { return el } -window.label = function(text) { +window.label = function(inside) { let el = document.createElement("label") - el.innerText = text + if(typeof inside === "string") { + el.innerText = inside + } else { + el.render = inside + } quill.render(el) return el } -window.textarea = function(placeholder) { +window.textarea = function(placeholder = "") { let el = document.createElement("textarea") el.placeholder = placeholder quill.render(el) @@ -845,8 +946,8 @@ HTMLElement.prototype.onAppear = function(func) { }; HTMLElement.prototype.onClick = function(func) { - const onMouseDown = () => func.call(this, false); - const onMouseUp = () => func.call(this, true); + const onMouseDown = (e) => func.call(this, false, e); + const onMouseUp = (e) => func.call(this, true, e); this._storeListener("mousedown", onMouseDown); this._storeListener("mouseup", onMouseUp); return this; @@ -929,34 +1030,38 @@ HTMLElement.prototype.onTap = function(cb) { }; /* WHY THIS LISTENER IS THE WAY IT IS: -- If we dispatch the "navigate" event on the window (as one would expect for a "navigate" event), a listener placed on the element will not pick it up. -- However, if we add the listener on the window, it won't have the "this" scope that a callback normally would. Which makes it much less useful. -- Then, if we try to add that scope using bind(), it makes the function.toString() unreadable, which means we cannot detect duplicate listeners. -- Therefore, we just have to attach the navigate event to the element, and manually trigger that when the window listener fires. +- We can't just put a listener on the element, because a window "navigate" event won't trigger it +- We can't just put a listener on the window, because the "this" variable will only refer to the window +- And, if we try to re-add that scope using bind(), it makes the return value of .toString() unreadable, which means we cannot detect duplicate listeners. +- Therefore, we attach a global navigate event to the window, and each navigate event in this array, and manually trigger each event when the global one fires. */ navigateListeners = [] -HTMLElement.prototype.onNavigate = function(cb) { - this._storeListener("navigate", cb); - - let found = false - for(entry of navigateListeners) { - if(entry.cb.toString() === cb.toString() && - this.nodeName === entry.el.nodeName) { - found = true - break; - } - } - if(found === false) { - navigateListeners.push({el: this, cb: cb}) - } - - return this; -}; window.addEventListener("navigate", () => { for(entry of navigateListeners) { entry.el.dispatchEvent(new CustomEvent("navigate")) } }) +HTMLElement.prototype.onNavigate = function(cb) { + this._storeListener("navigate", cb); + + let found = false + let elementIndex = Array.from(this.parentNode.children).indexOf(this) + for(entry of navigateListeners) { + if( + entry.cb.toString() === cb.toString() + && entry.index === elementIndex + && this.nodeName === entry.el.nodeName + ) { + found = true + break; + } + } + if(found === false) { + navigateListeners.push({el: this, cb: cb, index: elementIndex}) + } + + return this; +}; /* Same principle applies