Compare commits

...

11 Commits

Author SHA1 Message Date
sam
49b153b0ff Update README.md 2026-02-09 16:06:49 -06:00
sam
b801ac9da6 Update README.md 2026-02-09 16:06:27 -06:00
sam
656a673ca9 Update README.md 2026-01-23 19:10:29 -06:00
sam
61af976a7a Update README.md 2026-01-23 19:09:10 -06:00
metacryst
f1e8be1cd8 fix test log 2025-12-27 04:36:11 -06:00
metacryst
cee4d4fa8a fixing stack state problem 2025-12-27 02:35:48 -06:00
metacryst
188837d873 patching stack problem 2025-12-26 06:33:21 -06:00
metacryst
12bb5346e8 stack state working (when top-level parent) 2025-12-26 05:54:27 -06:00
metacryst
eb6975c7de state array working 2025-12-26 04:22:22 -06:00
metacryst
c4560aba37 state test working, added random experimental html 2025-12-26 01:36:04 -06:00
metacryst
b08e2767f6 12.16, 12.17 changes 2025-12-25 06:41:43 -06:00
8 changed files with 686 additions and 163 deletions

174
README.md
View File

@@ -1,4 +1,3 @@
<p align="center"> <p align="center">
<img src="VSCode/docs/Quill.png" alt="drawing" width="100"/> <img src="VSCode/docs/Quill.png" alt="drawing" width="100"/>
<h1 align="center">Quill</h1> <h1 align="center">Quill</h1>
@@ -8,48 +7,157 @@
Quill is a SwiftUI-style JavaScript framework. It makes use of components called Shadows, which are HTML Custom Elements. Quill is a SwiftUI-style JavaScript framework. It makes use of components called Shadows, which are HTML Custom Elements.
### Getting Started: ### Getting Started:
Go to https://github.com/capturedsun/template-quill and clone this repo. Take index.js and put it in your app. Typically as quill.js. Then import it in the head of the HTML.
### App: ### Basic Overview:
src/app.js is the application entry point. Any Shadows rendered inside will be rendered in the body.
### Rendering Elements: Quill uses components called Shadows. Each Shadow is a Custom HTML Element (https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements)
The basis of rendering is the Shadow, which extends HTMLElement. The Shadow is the equivalent of a React Component.
To create a Shadow, simply:
- Create a class which extends Shadow
- Include a render() function
- Register the shadow as an element
...
``` ```
class File extends Shadow { class Home extends Shadow {
render = () => { render() {
p(this.name)
p("asd")
} }
} }
register(File, "file-el") register(Home)
``` ```
## Needs Support: Once created, it can be imported like
Ternaries within render() ```
Other statements within render() import "Home.js"
Multiple-argument attributes in render() ```
(Not how we are NOT importing the actual class object. If that happens, it will fail.)
## Limitations: Here is an example of Hello World:
While rendering is underway, an element's state can only be accessed from within that element
## Boilerplate: ```
- ```*html```: Type in an HTML file and select the suggestion to create HTML boilerplate. class Home extends Shadow {
- ```*element```: Type in a JS file and select the suggestion to create JS Custom Element boilerplate. render() {
p("Hello World")
.x(50, vw)
.y(50, vh)
}
}
## Functions: register(Home)
Clone this repository into the top level of the project you are working on, so the HTML can find the "quill.js" functions. ```
Use backticks with both to get HTML and CSS syntax highlighting.
- ```css() or addStyle()```: Adds a style to a Quill style tag in the head. This will render a paragraph tag in the middle of the screen.
- ```html()```: Creates a parsed HTML element (which is not yet in the DOM)
Here's what it will look like in HTML:
```
<body>
<home->
<p style="position: absolute; top: 50vh; left: 50vw;">Hello World</p>
</home->
</body>
```
Note: .x() and .y() are quill-specific functions that were created simply for nice syntax. However, this would also be valid:
```
p("Hello World")
.top(50, vh)
.left(50, vw)
```
There are quill functions for every HTML style attribute. If they have units, they will follow the pattern directly above, where the first parameter is the amount and the second parameter is the unit.
### Real Basic Example:
First, you need your index.html. Here is one:
```
<!DOCTYPE html>
<html lang="en" class="public">
<head>
<title>Parchment</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="/_/icons/quill.svg">
<link rel="stylesheet" href="/_/code/shared.css">
<script src="/_/code/quill.js"></script>
<script type="module" src="75820185/index.js"></script>
</head>
<body>
</body>
</html>
```
When starting, it is typical to make a "Home" shadow and import it in index.js. Here is an example:
index.js:
```
import "./Home.js"
Home()
```
Home.js:
```
import "../components/NavBar.js"
import "./HomeContent.js"
import "./Why.js"
import "./Events.js"
import "./Join.js"
import "./SignIn.js"
import "./Success.js"
class Home extends Shadow {
render() {
ZStack(() => {
NavBar()
img("/_/icons/logo.svg", "2.5em")
.onClick((done) => {
if(!done) return
window.navigateTo("/")
})
.position("absolute")
.left(50, vw).top(4, em)
.center()
.transform(`translate(${window.isMobile() ? "-50%" : "-2em"}, -50%)`)
switch(window.location.pathname) {
case "/":
HomeContent()
break;
case "/why":
Why()
break;
case "/events":
Events()
break;
case "/join":
Join()
break;
case "/success":
Success()
break;
}
})
.onNavigate(() => {
this.rerender()
})
}
}
register(Home)
```
Success.js:
```
class Success extends Shadow {
render() {
p("Thanks for your purchase! You will receive a confirmation email shortly. <br><br> <b>Keep that email; it will be checked at the door.</b>")
.x(50, vw).y(50, vh)
.center()
}
}
register(Success)
```

View File

@@ -1,7 +0,0 @@
class Home extends Page {
render = () => {
}
}
export default Home

73
Test/state/state.test.js Normal file
View File

@@ -0,0 +1,73 @@
window.testSuites.push( class testState {
SimpleState() {
class Home extends Shadow {
state = {
pathname: "/"
}
render() {
VStack(() => {
p("hi")
.top(() => {return (this.state.pathname === "/" ? [11, vw] : [7, vw])})
})
.onAppear(() => {
this.state.pathname = "/asd"
})
}
}
register(Home, randomName("home"))
window.Home()
if(!($("p").style.top === "7vw")) return "state was not respeccted"
}
StateArrayPush() {
class Home extends Shadow {
state = {
logs: []
}
render() {
VStack(() => {
p("hi")
.fontSize(() => {return this.state.logs.length > 0 ? [2, em] : [1, em]})
})
.onAppear(() => {
this.state.logs.push("one")
})
}
}
register(Home, randomName("home"))
window.Home()
if(!($("p").style.fontSize === "2em")) return "state did not update!"
}
SimpleStack() {
class Home extends Shadow {
state = {
logs: []
}
render() {
VStack(() => {
this.state.logs.forEach((log) => {
p(log)
})
})
.onAppear(() => {
this.state.logs.push("one")
this.state.logs.push("two")
})
}
}
register(Home, randomName("home"))
window.Home()
if(!$("p")) return "no p's rendered"
if($$("p")[0].innerText !== "one") return "state did not update!"
if($$("p")[1].innerText !== "two") return "state did not update!"
}
})

View File

@@ -2,17 +2,10 @@
<html lang="en"> <html lang="en">
<head> <head>
<title>Quill</title> <title>Quill</title>
<link rel="icon" href=""> <link rel="icon" href="../_/Quill.png">
<link rel="stylesheet" href=""> <link rel="stylesheet" href="">
<script src="../index.js"></script> <script src="../index.js"></script>
<script src="test.js" type="module"></script> <script src="test.js" type="module"></script>
<script type="module">
import Home from "./Pages/home.js";
window.routes = {
"/Test": Home
}
</script>
</head> </head>
<body style="background: rgb(242, 194, 147)"> <body style="background: rgb(242, 194, 147)">

View File

@@ -1,13 +1,7 @@
console.log("Tests initializing.") console.log("Tests initializing.")
window.testSuites = []; window.testSuites = [];
await import ("./Skeleton/parse.test.js") await import ("./state/state.test.js")
await import ("./Skeleton/init.test.js")
await import ("./Skeleton/observedobject.test.js")
await import ("./Skeleton/parserender.test.js")
await import ("./Skeleton/state.test.js")
await import ("./Element/stacks.test.js")
await import ("./Element/Group.test.js")
window.randomName = function randomName(prefix) { window.randomName = function randomName(prefix) {
const sanitizedPrefix = prefix.toLowerCase().replace(/[^a-z0-9]/g, ''); const sanitizedPrefix = prefix.toLowerCase().replace(/[^a-z0-9]/g, '');
@@ -43,6 +37,7 @@ window.test = async function() {
let test = suiteContents[i]; let test = suiteContents[i];
if(typeof suite[test] === 'function' && test !== "constructor") { if(typeof suite[test] === 'function' && test !== "constructor") {
testNum++; testNum++;
document.body.innerHTML = ""
console.log(`%c${testNum}. ${test}`, "margin-top: 10px; border-top: 2px solid #e9c9a0; color: #e9c9a0; border-radius: 10px; padding: 10px;"); console.log(`%c${testNum}. ${test}`, "margin-top: 10px; border-top: 2px solid #e9c9a0; color: #e9c9a0; border-radius: 10px; padding: 10px;");
let fail; let fail;
@@ -78,12 +73,12 @@ window.test = async function() {
console.log("") console.log("")
let elapsed = new Date() - start; let elapsed = new Date() - start;
if(failed === 0) { if(failed === 0) {
console.log(`%cRan ${failed+success} tests in ${elapsed}ms`, 'color: #00FF00'); console.log(`%cRan ${failed+success} tests in ${elapsed}ms`, 'color: #9cd499ff');
console.log(`%c ${success} passed`, 'color: #00FF00'); console.log(`%c ${success} passed`, 'color: #9cd499ff');
console.log(`%c ${failed} failed`); console.log(` ${failed} failed`);
} else { } else {
console.log(`%cRan ${failed+success} tests in ${elapsed}ms`, 'color: rgb(254, 62, 43)'); console.log(`%cRan ${failed+success} tests in ${elapsed}ms`, 'color: rgb(254, 62, 43)');
console.log(`%c ${success} `, 'color: #00FF00', "passed"); console.log(`%c ${success} `, 'color: #9cd499ff', "passed");
console.log(`%c ${failed} failed`, 'color: rgb(254, 62, 43)'); console.log(`%c ${failed} failed`, 'color: rgb(254, 62, 43)');
} }
} }

BIN
_/Quill.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

229
experiment.html Normal file
View File

@@ -0,0 +1,229 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Canvas Text Editor</title>
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
background: #d8cbb0;
overflow: hidden;
}
canvas {
display: block;
background: #e7dcc6;
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
const canvas = document.getElementById("c");
const ctx = canvas.getContext("2d");
const dpr = window.devicePixelRatio || 1;
function resize() {
const w = window.innerWidth;
const h = window.innerHeight;
canvas.style.width = w + "px";
canvas.style.height = h + "px";
canvas.width = Math.floor(w * dpr);
canvas.height = Math.floor(h * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
window.addEventListener("resize", resize);
resize();
/* =====================
Editor State
===================== */
const fontSize = 13;
const lineHeight = 20;
const padding = 20;
const font = `${fontSize}px monospace`;
let lines = [""];
let cursor = { line: 0, col: 0 };
let blink = true;
let blinkEnabled = true;
let typingTimeout = null;
/* =====================
Rendering
===================== */
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#631414";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.font = font;
ctx.textBaseline = "top";
ctx.fillStyle = "#d2531a";
for (let i = 0; i < lines.length; i++) {
ctx.fillText(
lines[i],
padding,
padding + i * lineHeight
);
}
if (blink) {
const before = lines[cursor.line].slice(0, cursor.col);
const x = padding + ctx.measureText(before).width;
const y = padding - 2 + cursor.line * lineHeight;
ctx.fillRect(x, y, 2, fontSize + 2);
}
}
/* =====================
Cursor Blink Logic
===================== */
setInterval(() => {
if (!blinkEnabled) return;
blink = !blink;
draw();
}, 500);
function stopBlinkWhileTyping() {
blinkEnabled = false;
blink = true;
draw();
clearTimeout(typingTimeout);
typingTimeout = setTimeout(() => {
blinkEnabled = true;
blink = true;
draw();
}, 600);
}
/* =====================
Helpers
===================== */
function clampCursor() {
cursor.line = Math.max(0, Math.min(cursor.line, lines.length - 1));
cursor.col = Math.max(0, Math.min(cursor.col, lines[cursor.line].length));
}
function insertText(text) {
const line = lines[cursor.line];
lines[cursor.line] =
line.slice(0, cursor.col) +
text +
line.slice(cursor.col);
cursor.col += text.length;
}
function newline() {
const line = lines[cursor.line];
lines[cursor.line] = line.slice(0, cursor.col);
lines.splice(cursor.line + 1, 0, line.slice(cursor.col));
cursor.line++;
cursor.col = 0;
}
function backspace() {
if (cursor.col > 0) {
const line = lines[cursor.line];
lines[cursor.line] =
line.slice(0, cursor.col - 1) +
line.slice(cursor.col);
cursor.col--;
} else if (cursor.line > 0) {
const prevLen = lines[cursor.line - 1].length;
lines[cursor.line - 1] += lines[cursor.line];
lines.splice(cursor.line, 1);
cursor.line--;
cursor.col = prevLen;
}
}
/* =====================
Keyboard Input
===================== */
window.addEventListener("keydown", (e) => {
if (e.metaKey || e.ctrlKey) return;
let changed = true;
switch (e.key) {
case "ArrowLeft": cursor.col--; break;
case "ArrowRight": cursor.col++; break;
case "ArrowUp": cursor.line--; break;
case "ArrowDown": cursor.line++; break;
case "Backspace": backspace(); e.preventDefault(); break;
case "Enter": newline(); e.preventDefault(); break;
case "Tab": insertText(" "); e.preventDefault(); break;
default:
if (e.key.length === 1) {
insertText(e.key);
e.preventDefault();
} else {
changed = false;
}
}
clampCursor();
if (changed) {
stopBlinkWhileTyping();
}
draw();
});
/* =====================
Mouse → Cursor
===================== */
canvas.addEventListener("mousedown", (e) => {
ctx.font = font;
const mx = e.offsetX - padding;
const my = e.offsetY - padding;
cursor.line = Math.max(
0,
Math.min(Math.floor(my / lineHeight), lines.length - 1)
);
let x = 0;
cursor.col = 0;
for (let i = 0; i < lines[cursor.line].length; i++) {
const w = ctx.measureText(lines[cursor.line][i]).width;
if (x + w / 2 >= mx) break;
x += w;
cursor.col++;
}
blink = true;
blinkEnabled = true;
draw();
});
/* =====================
Initial Draw
===================== */
draw();
</script>
</body>
</html>

340
index.js
View File

@@ -1,6 +1,10 @@
/* /*
Sam Russell Sam Russell
Captured Sun Captured Sun
12.26.25 - Fixing stack state problem
12.25.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.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.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 11.24.25 - Fixing onClick because it was reversed, adding event to onHover params
@@ -68,6 +72,12 @@ window.$ = function(selector, el = document) {
window.$$ = function(selector, el = document) { window.$$ = function(selector, el = document) {
return Array.from(el.querySelectorAll(selector)) return Array.from(el.querySelectorAll(selector))
} }
HTMLElement.prototype.$$ = function(selector) {
return window.$$(selector, this)
}
DocumentFragment.prototype.$$ = function(selector) {
return window.$$(selector, this)
}
/* CONSOLE */ /* CONSOLE */
@@ -163,6 +173,7 @@ Object.defineProperty(Array.prototype, 'last', {
window.quill = { window.quill = {
rendering: [], rendering: [],
lastState: null,
render: (el) => { render: (el) => {
if(el instanceof Shadow) { if(el instanceof Shadow) {
@@ -193,17 +204,11 @@ window.quill = {
quill.rendering.pop() quill.rendering.pop()
}, },
loadPage: () => { rerenderStackContents: (el, cb) => {
let URL = window.location.pathname el.innerHTML = ""
if(!window.routes[URL]) { quill.rendering.push(el)
throw new Error("No URL for this route: ", URL) cb()
} quill.rendering.pop()
let pageClass = window[routes[URL]]
document.title = pageClass.title ?? document.title
document.body.innerHTML = ""
let page = new pageClass()
quill.render(page)
}, },
isStack: (el) => { isStack: (el) => {
@@ -216,6 +221,7 @@ window.Shadow = class extends HTMLElement {
super() super()
} }
} }
window.register = (el, tagname) => { window.register = (el, tagname) => {
if (typeof el.prototype.render !== 'function') { if (typeof el.prototype.render !== 'function') {
throw new Error("Element must have a render: " + el.prototype.constructor.name) throw new Error("Element must have a render: " + el.prototype.constructor.name)
@@ -230,6 +236,75 @@ window.register = (el, tagname) => {
window[el.prototype.constructor.name] = function (...params) { window[el.prototype.constructor.name] = function (...params) {
let instance = new el(...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) quill.render(instance)
return instance return instance
} }
@@ -314,6 +389,7 @@ function extendHTMLElementWithStyleSetters() {
case "marginRight": case "marginRight":
case "textUnderlineOffset": case "textUnderlineOffset":
case "letterSpacing":
return "unit-number" return "unit-number"
@@ -333,24 +409,27 @@ function extendHTMLElementWithStyleSetters() {
switch (type) { switch (type) {
case "unit-number": case "unit-number":
HTMLElement.prototype[prop] = function(value, unit = "px") { HTMLElement.prototype[prop] = StyleFunction(function(value, unit = "px") {
if ((typeof value !== "number" || isNaN(value)) && value !== "auto") {
throw new Error(`Invalid value for ${prop}: ${value}. Expected a number.`);
}
if(value === "auto") { if(value === "auto") {
this.style[prop] = value this.style[prop] = value
return this return this
} }
this.style[prop] = value + unit; this.style[prop] = value + unit;
if (value !== "" && this.style[prop] === "") {
throw new Error(`Invalid CSS value for ${prop}: ` + value + unit);
}
return this; return this;
}; });
break; break;
case "string": case "string":
HTMLElement.prototype[prop] = function(value) { HTMLElement.prototype[prop] = StyleFunction(function(value) {
this.style[prop] = value; this.style[prop] = value;
if (value !== "" && this.style[prop] === "") {
throw new Error(`Invalid CSS value for ${prop}: ` + value);
}
return this; return this;
}; });
break; break;
} }
}); });
@@ -358,46 +437,86 @@ function extendHTMLElementWithStyleSetters() {
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) { HTMLElement.prototype.styles = function(cb) {
cb.call(this, this) cb.call(this, this)
return this return this
} }
HTMLElement.prototype.paddingVertical = function(value, unit = "px") { /* Type 1 */
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`); HTMLElement.prototype.paddingVertical = StyleFunction(function(value, unit = "px") {
this.style.paddingTop = value + unit this.style.paddingTop = value + unit
this.style.paddingBottom = value + unit this.style.paddingBottom = value + unit
return this return this
} })
HTMLElement.prototype.paddingHorizontal = function(value, unit = "px") { HTMLElement.prototype.paddingHorizontal = StyleFunction(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 this.style.paddingRight = value + unit
this.style.paddingLeft = value + unit this.style.paddingLeft = value + unit
return this return this
} })
HTMLElement.prototype.marginVertical = function(value, unit = "px") { HTMLElement.prototype.marginVertical = StyleFunction(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 this.style.marginTop = value + unit
this.style.marginBottom = value + unit this.style.marginBottom = value + unit
return this return this
} })
HTMLElement.prototype.marginHorizontal = function(value, unit = "px") { HTMLElement.prototype.marginHorizontal = StyleFunction(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 this.style.marginRight = value + unit
this.style.marginLeft = value + unit this.style.marginLeft = value + unit
return this return this
} })
HTMLElement.prototype.fontSize = function(value, unit = "px") { HTMLElement.prototype.fontSize = StyleFunction(function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
switch(value) { switch(value) {
case "6xl": case "6xl":
@@ -441,6 +560,27 @@ HTMLElement.prototype.fontSize = function(value, unit = "px") {
} }
this.style.fontSize = value + unit this.style.fontSize = value + unit
return this 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) { function checkPositionType(el) {
@@ -661,7 +801,7 @@ window.form = function(cb) {
return el return el
} }
window.input = function(placeholder, width, height) { window.input = function(placeholder = "", width, height) {
let el = document.createElement("input") let el = document.createElement("input")
el.placeholder = placeholder el.placeholder = placeholder
el.style.width = width el.style.width = width
@@ -670,14 +810,18 @@ window.input = function(placeholder, width, height) {
return el return el
} }
window.label = function(text) { window.label = function(inside) {
let el = document.createElement("label") let el = document.createElement("label")
el.innerText = text if(typeof inside === "string") {
el.innerText = inside
} else {
el.render = inside
}
quill.render(el) quill.render(el)
return el return el
} }
window.textarea = function(placeholder) { window.textarea = function(placeholder = "") {
let el = document.createElement("textarea") let el = document.createElement("textarea")
el.placeholder = placeholder el.placeholder = placeholder
quill.render(el) quill.render(el)
@@ -687,25 +831,33 @@ window.textarea = function(placeholder) {
/* STACKS */ /* STACKS */
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(name)
quill.lastState = null
cb()
if(quill.lastState) {
nowRendering.addStateWatcher(quill.lastState, quill.rerenderStackContents.bind(nowRendering, nowRendering, 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
}
}
window.VStack = function (cb = () => {}) { window.VStack = function (cb = () => {}) {
let styles = ` let styles = `
display: flex; display: flex;
flex-direction: column; flex-direction: column;
` `
let nowRendering = quill.rendering[quill.rendering.length-1] return handleStack(cb, "VStack", styles)
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 = () => {}) { window.HStack = function (cb = () => {}) {
@@ -713,35 +865,11 @@ window.HStack = function (cb = () => {}) {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
`; `;
let nowRendering = quill.rendering[quill.rendering.length - 1]; return handleStack(cb, "HStack", styles)
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 = () => {}) { window.ZStack = function (cb = () => {}) {
let nowRendering = quill.rendering[quill.rendering.length - 1]; return handleStack(cb, "ZStack")
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;
}; };
/* SHAPES */ /* SHAPES */
@@ -845,8 +973,8 @@ HTMLElement.prototype.onAppear = function(func) {
}; };
HTMLElement.prototype.onClick = function(func) { HTMLElement.prototype.onClick = function(func) {
const onMouseDown = () => func.call(this, false); const onMouseDown = (e) => func.call(this, false, e);
const onMouseUp = () => func.call(this, true); const onMouseUp = (e) => func.call(this, true, e);
this._storeListener("mousedown", onMouseDown); this._storeListener("mousedown", onMouseDown);
this._storeListener("mouseup", onMouseUp); this._storeListener("mouseup", onMouseUp);
return this; return this;
@@ -929,34 +1057,38 @@ HTMLElement.prototype.onTap = function(cb) {
}; };
/* WHY THIS LISTENER IS THE WAY IT IS: /* 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. - We can't just put a listener on the element, because a window "navigate" event won't trigger it
- 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. - We can't just put a listener on the window, because the "this" variable will only refer to the window
- Then, if we try to add that scope using bind(), it makes the function.toString() unreadable, which means we cannot detect duplicate listeners. - 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 just have to attach the navigate event to the element, and manually trigger that when the window listener fires. - 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 = [] 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", () => { window.addEventListener("navigate", () => {
for(entry of navigateListeners) { for(entry of navigateListeners) {
entry.el.dispatchEvent(new CustomEvent("navigate")) 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 Same principle applies