Compare commits
11 Commits
ed6d885557
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 49b153b0ff | |||
| b801ac9da6 | |||
| 656a673ca9 | |||
| 61af976a7a | |||
|
|
f1e8be1cd8 | ||
|
|
cee4d4fa8a | ||
|
|
188837d873 | ||
|
|
12bb5346e8 | ||
|
|
eb6975c7de | ||
|
|
c4560aba37 | ||
|
|
b08e2767f6 |
174
README.md
174
README.md
@@ -1,4 +1,3 @@
|
||||
|
||||
<p align="center">
|
||||
<img src="VSCode/docs/Quill.png" alt="drawing" width="100"/>
|
||||
<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.
|
||||
|
||||
### 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:
|
||||
src/app.js is the application entry point. Any Shadows rendered inside will be rendered in the body.
|
||||
### Basic Overview:
|
||||
|
||||
### Rendering 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
|
||||
|
||||
...
|
||||
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)
|
||||
```
|
||||
class File extends Shadow {
|
||||
render = () => {
|
||||
p(this.name)
|
||||
p("asd")
|
||||
class Home extends Shadow {
|
||||
render() {
|
||||
}
|
||||
}
|
||||
|
||||
register(File, "file-el")
|
||||
register(Home)
|
||||
```
|
||||
|
||||
## Needs Support:
|
||||
Ternaries within render()
|
||||
Other statements within render()
|
||||
Multiple-argument attributes in render()
|
||||
Once created, it can be imported like
|
||||
```
|
||||
import "Home.js"
|
||||
```
|
||||
(Not how we are NOT importing the actual class object. If that happens, it will fail.)
|
||||
|
||||
## Limitations:
|
||||
While rendering is underway, an element's state can only be accessed from within that element
|
||||
Here is an example of Hello World:
|
||||
|
||||
## Boilerplate:
|
||||
- ```*html```: Type in an HTML file and select the suggestion to create HTML boilerplate.
|
||||
- ```*element```: Type in a JS file and select the suggestion to create JS Custom Element boilerplate.
|
||||
```
|
||||
class Home extends Shadow {
|
||||
render() {
|
||||
p("Hello World")
|
||||
.x(50, vw)
|
||||
.y(50, vh)
|
||||
}
|
||||
}
|
||||
|
||||
## Functions:
|
||||
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.
|
||||
- ```html()```: Creates a parsed HTML element (which is not yet in the DOM)
|
||||
register(Home)
|
||||
```
|
||||
|
||||
This will render a paragraph tag in the middle of the screen.
|
||||
|
||||
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)
|
||||
```
|
||||
@@ -1,7 +0,0 @@
|
||||
class Home extends Page {
|
||||
|
||||
render = () => {
|
||||
}
|
||||
}
|
||||
|
||||
export default Home
|
||||
73
Test/state/state.test.js
Normal file
73
Test/state/state.test.js
Normal 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!"
|
||||
}
|
||||
})
|
||||
@@ -2,17 +2,10 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Quill</title>
|
||||
<link rel="icon" href="">
|
||||
<link rel="icon" href="../_/Quill.png">
|
||||
<link rel="stylesheet" href="">
|
||||
<script src="../index.js"></script>
|
||||
<script src="test.js" type="module"></script>
|
||||
<script type="module">
|
||||
import Home from "./Pages/home.js";
|
||||
|
||||
window.routes = {
|
||||
"/Test": Home
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body style="background: rgb(242, 194, 147)">
|
||||
|
||||
|
||||
17
Test/test.js
17
Test/test.js
@@ -1,13 +1,7 @@
|
||||
console.log("Tests initializing.")
|
||||
window.testSuites = [];
|
||||
|
||||
await import ("./Skeleton/parse.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")
|
||||
await import ("./state/state.test.js")
|
||||
|
||||
window.randomName = function randomName(prefix) {
|
||||
const sanitizedPrefix = prefix.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
@@ -43,6 +37,7 @@ window.test = async function() {
|
||||
let test = suiteContents[i];
|
||||
if(typeof suite[test] === 'function' && test !== "constructor") {
|
||||
testNum++;
|
||||
document.body.innerHTML = ""
|
||||
console.log(`%c${testNum}. ${test}`, "margin-top: 10px; border-top: 2px solid #e9c9a0; color: #e9c9a0; border-radius: 10px; padding: 10px;");
|
||||
|
||||
let fail;
|
||||
@@ -78,12 +73,12 @@ window.test = async function() {
|
||||
console.log("")
|
||||
let elapsed = new Date() - start;
|
||||
if(failed === 0) {
|
||||
console.log(`%cRan ${failed+success} tests in ${elapsed}ms`, 'color: #00FF00');
|
||||
console.log(`%c ${success} passed`, 'color: #00FF00');
|
||||
console.log(`%c ${failed} failed`);
|
||||
console.log(`%cRan ${failed+success} tests in ${elapsed}ms`, 'color: #9cd499ff');
|
||||
console.log(`%c ${success} passed`, 'color: #9cd499ff');
|
||||
console.log(` ${failed} failed`);
|
||||
} else {
|
||||
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)');
|
||||
}
|
||||
}
|
||||
|
||||
BIN
_/Quill.png
Normal file
BIN
_/Quill.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 117 KiB |
229
experiment.html
Normal file
229
experiment.html
Normal 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
340
index.js
@@ -1,6 +1,10 @@
|
||||
/*
|
||||
Sam Russell
|
||||
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 - 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 +72,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 +173,7 @@ Object.defineProperty(Array.prototype, 'last', {
|
||||
|
||||
window.quill = {
|
||||
rendering: [],
|
||||
lastState: null,
|
||||
|
||||
render: (el) => {
|
||||
if(el instanceof Shadow) {
|
||||
@@ -193,17 +204,11 @@ 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)
|
||||
rerenderStackContents: (el, cb) => {
|
||||
el.innerHTML = ""
|
||||
quill.rendering.push(el)
|
||||
cb()
|
||||
quill.rendering.pop()
|
||||
},
|
||||
|
||||
isStack: (el) => {
|
||||
@@ -216,6 +221,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 +236,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 +389,7 @@ function extendHTMLElementWithStyleSetters() {
|
||||
case "marginRight":
|
||||
|
||||
case "textUnderlineOffset":
|
||||
case "letterSpacing":
|
||||
|
||||
return "unit-number"
|
||||
|
||||
@@ -333,24 +409,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 +437,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 +560,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 +801,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 +810,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 +831,45 @@ 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, 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
|
||||
}
|
||||
}
|
||||
|
||||
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 +973,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 +1057,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
|
||||
|
||||
Reference in New Issue
Block a user