This commit is contained in:
metacryst
2025-12-26 19:55:31 -06:00
27 changed files with 435 additions and 1080 deletions

View File

@@ -1,6 +1,9 @@
/*
Sam Russell
Captured Sun
12.26.25 - State for arrays, nested objects. State for stacks (Shadow-only)
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 +71,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 +172,7 @@ Object.defineProperty(Array.prototype, 'last', {
window.quill = {
rendering: [],
lastState: null,
render: (el) => {
if(el instanceof Shadow) {
@@ -193,19 +203,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 +213,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 +228,75 @@ window.register = (el, tagname) => {
window[el.prototype.constructor.name] = function (...params) {
let instance = new el(...params)
if(instance.state) {
const proxyCache = new WeakMap();
function reactive(value, path=[]) {
if (value && typeof value === "object") {
if (proxyCache.has(value)) return proxyCache.get(value);
const p = new Proxy(value, createHandlers(path));
proxyCache.set(value, p);
return p;
}
return value;
}
function isNumericKey(prop) {
return typeof prop === "string" && prop !== "" && String(+prop) === prop;
}
function createHandlers(path) {
return {
get(target, prop, receiver) {
if (typeof prop === "symbol") {
return Reflect.get(target, prop, receiver);
}
let nextPath = (Array.isArray(target) && !isNumericKey(prop)) ? path : path.concat(prop) // To filter out arr.length, arr.map, arr.forEach, etc.
quill.lastState = nextPath.join(".");
const v = Reflect.get(target, prop, receiver);
return reactive(v, nextPath);
},
set(target, prop, value, receiver) {
const oldLength = Array.isArray(target) ? target.length : undefined;
const oldValue = target[prop];
if (oldValue === value) return true;
const result = Reflect.set(target, prop, value, receiver);
let changedPath = (Array.isArray(target) && (!isNumericKey(prop) || target.length !== oldLength)) ? path : path.concat(prop).join("."); // To filter out arr.length, arr.map, arr.forEach, and also a push/pop/unshift.
const watchers = instance.stateWatchers[changedPath];
if (watchers) {
watchers.forEach(cb => cb());
}
return result;
}
};
}
let proxy = reactive(instance.state)
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 +381,7 @@ function extendHTMLElementWithStyleSetters() {
case "marginRight":
case "textUnderlineOffset":
case "letterSpacing":
return "unit-number"
@@ -333,24 +401,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 +429,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,6 +552,27 @@ HTMLElement.prototype.fontSize = function(value, unit = "px") {
}
this.style.fontSize = 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
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
}
function checkPositionType(el) {
@@ -661,7 +793,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 +802,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)
@@ -687,61 +823,48 @@ window.textarea = function(placeholder) {
/* STACKS */
window.VStack = function (cb = () => {}) {
let styles = `
display: flex;
flex-direction: column;
`
handleStack = function(cb, name, styles="") {
let nowRendering = quill.rendering[quill.rendering.length-1]
if (nowRendering.innerHTML.trim() === "" && !quill.isStack(nowRendering)) {
nowRendering.style.cssText += styles
nowRendering.classList.add("VStack")
nowRendering.classList.add(name)
quill.lastState = null
cb()
if(quill.lastState) {
nowRendering.addStateWatcher(quill.lastState, () => {
nowRendering.innerHTML = ""
cb()
})
}
return nowRendering
} else {
let div = document.createElement("div")
div.classList.add(name)
div.style.cssText += styles
div.render = cb
quill.render(div)
return div
}
}
let div = document.createElement("div")
div.classList.add("VStack")
div.style.cssText += styles
div.render = cb
quill.render(div)
return div
window.VStack = function (cb = () => {}) {
let styles = `
display: flex;
flex-direction: column;
`
return handleStack(cb, "VStack", styles)
}
window.HStack = function (cb = () => {}) {
let styles = `
display: flex;
flex-direction: row;
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;
return handleStack(cb, "HStack", styles)
};
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;
return handleStack(cb, "ZStack")
};
/* SHAPES */
@@ -845,8 +968,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 +1052,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

View File

@@ -43,10 +43,10 @@
}
body {
background-color: #2B311A;
color: var(--accent);
font-family: 'Bona Nova', sans-serif;
font-size: 16px;
background-color: var(--main);
color: var(--accent);
}
#title {

8
ui/_/code/zod_4.2.1.js Normal file

File diff suppressed because one or more lines are too long