From 0c935f3c25cb6826525d7c6c31e9027c481330e2 Mon Sep 17 00:00:00 2001 From: CoolElectronics Date: Sat, 27 Jan 2024 17:34:50 -0500 Subject: [PATCH] split css html and js --- .gitignore | 1 + AliceJS.js | 626 +-------------------------------------------------- css.js | 79 +++++++ csspost.js | 2 + html.js | 65 ++++++ js.js | 483 +++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 7 files changed, 634 insertions(+), 624 deletions(-) create mode 100644 css.js create mode 100644 csspost.js create mode 100644 html.js create mode 100644 js.js diff --git a/.gitignore b/.gitignore index 165c8fe..06eb460 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ examples/lib/ +dist/ node_modules index.js a.js diff --git a/AliceJS.js b/AliceJS.js index cb37f02..3ed4c5a 100644 --- a/AliceJS.js +++ b/AliceJS.js @@ -1,623 +1,3 @@ -// The global list of "references", as captured by the proxy in stateful -let __reference_stack = []; - -// We add some extra properties into various objects throughout, better to use symbols and not interfere -let ALICEJS_REFERENCES_MAPPING = Symbol(); -let ALICEJS_REFERENCES_MARKER = Symbol(); -let ALICEJS_STATEFUL_LISTENERS = Symbol(); - -// Say you have some code like -//// let state = stateful({ -//// a: 1 -//// }) -//// let elm =

{window.use(state.a)}

-// -// According to the standard, the order of events is as follows: -// - the getter for window.use gets called, setting __reference_stack to an empty list -// - the proxy for state.a is triggered, pushing { state, "a", Proxy(state) } onto __reference_stack -// - the function that the getter returns is called, popping everything off the stack -// - the JSX factory h() is now passed the *reference* of state.a, not the value -Object.defineProperty(window, "use", { - get: () => { - __reference_stack = []; - return (_sink, mapping) => { - let references = __reference_stack; - __reference_stack = []; - - references[ALICEJS_REFERENCES_MARKER] = true; - if (mapping) references[ALICEJS_REFERENCES_MAPPING] = mapping; - return references; - }; - } -}); -Object.assign(window, { h, html, stateful, handle, useValue, css, rule, styled: { new: css, rule: rule } }); - -// This wraps the target in a proxy, doing 2 things: -// - whenever a property is accessed, update the reference stack -// - whenever a property is set, notify the subscribed listeners -// This is what makes our "pass-by-reference" magic work -export function stateful(target) { - target[ALICEJS_STATEFUL_LISTENERS] = []; - - const proxy = new Proxy(target, { - get(target, property, proxy) { - __reference_stack.push({ target, property, proxy }); - return Reflect.get(target, property, proxy); - }, - set(target, property, val) { - for (const listener of target[ALICEJS_STATEFUL_LISTENERS]) { - listener(target, property, val); - } - return Reflect.set(target, property, val); - }, - }); - - return proxy; -} - -function isAJSReferences(arr) { - return arr instanceof Array && ALICEJS_REFERENCES_MARKER in arr -} - -// This lets you subscribe to a stateful object -export function handle(references, callback) { - if (!isAJSReferences(references)) - throw new Error("Not an AliceJS reference set!"); - - if (ALICEJS_REFERENCES_MAPPING in references) { - const mapping = references[ALICEJS_REFERENCES_MAPPING]; - const used_props = []; - const used_targets = []; - - const values = new Map(); - - const pairs = []; - - const partial_update = (target, prop, val) => { - if (used_props.includes(prop) && used_targets.includes(target)) { - values.get(target)[prop] = val; - } - }; - - const full_update = () => { - const flattened_values = pairs.map( - (pair) => values.get(pair[0])[pair[1]], - ); - - const value = mapping(...flattened_values.reverse()); - - callback(value); - }; - - for (const p of references) { - const target = p.target; - const prop = p.property; - - used_props.push(prop); - used_targets.push(target); - - pairs.push([target, prop]); - - if (!values.has(target)) { - values.set(target, {}); - } - - partial_update(target, prop, target[prop]); - - target[ALICEJS_STATEFUL_LISTENERS].push((t, p, v) => { - partial_update(t, p, v); - full_update(); - }); - } - full_update(); - } else { - const reference = references[references.length - 1]; - const subscription = (target, prop, val) => { - if (prop === reference.property && target === reference.target) { - callback(val); - } - }; - reference.target[ALICEJS_STATEFUL_LISTENERS].push(subscription); - subscription(reference.target, reference.property, reference.target[reference.property]); - } -} - -export function useValue(references) { - let reference = references[references.length - 1]; - return reference.proxy[reference.property]; -} - -// Actual JSX factory. Responsible for creating the HTML elements and all of the *reactive* syntactic sugar -export function h(type, props, ...children) { - if (typeof type === "function") { - let newthis = stateful(Object.create(type.prototype)); - - for (const name in props) { - const references = props[name]; - if (isAJSReferences(references) && name.startsWith("bind:")) { - let reference = references[references.length - 1]; - const propname = name.substring(5); - if (propname == "this") { - reference.proxy[reference.property] = newthis; - } else { - // component two way data binding!! (exact same behavior as svelte:bind) - let isRecursive = false; - - handle(references, value => { - if (isRecursive) { - isRecursive = false; - return; - } - isRecursive = true; - newthis[propname] = value - }); - handle(use(newthis[propname]), value => { - if (isRecursive) { - isRecursive = false; - return; - } - isRecursive = true; - reference.proxy[reference.property] = value; - }); - } - delete props[name]; - } - } - Object.assign(newthis, props); - - let slot = []; - for (const child of children) { - JSXAddChild(child, slot.push.bind(slot)); - } - - let elm = type.apply(newthis, [slot]); - elm.$ = newthis; - newthis.root = elm; - if (newthis.css) { - elm.classList.add(newthis.css); - elm.classList.add("self"); - } - return elm; - } - - - const elm = document.createElement(type); - - for (const child of children) { - JSXAddChild(child, elm.appendChild.bind(elm)); - } - - if (!props) return elm; - - function useProp(name, callback) { - if (!(name in props)) return; - let prop = props[name]; - callback(prop); - delete props[name]; - } - - // insert an element at the start - useProp("before", callback => { - JSXAddChild(callback()); - }) - - // if/then/else syntax - useProp("if", condition => { - let thenblock = props["then"]; - let elseblock = props["else"]; - - if (isAJSReferences(condition)) { - if (thenblock) elm.appendChild(thenblock); - if (elseblock) elm.appendChild(elseblock); - - handle(condition, val => { - if (thenblock) { - if (val) { - thenblock.style.display = ""; - if (elseblock) elseblock.style.display = "none"; - } else { - thenblock.style.display = "none"; - - if (elseblock) elseblock.style.display = ""; - } - } else { - if (val) { - elm.style.display = ""; - } else { - elm.style.display = "none"; - } - } - }); - } else { - if (thenblock) { - if (condition) { - elm.appendChild(thenblock); - } else if (elseblock) { - elm.appendChild(elseblock); - } - } else { - if (condition) { - elm.appendChild(thenblock); - } else if (elseblock) { - elm.appendChild(elseblock); - } else { - elm.style.display = "none"; - return document.createTextNode(""); - } - } - } - - delete props["then"]; - delete props["else"]; - }); - - if ("for" in props && "do" in props) { - const predicate = props["for"]; - const closure = props["do"]; - - if (isAJSReferences(predicate)) { - const __elms = []; - let lastpredicate = []; - handle(predicate, val => { - if ( - Object.keys(val).length && - Object.keys(val).length == lastpredicate.length - ) { - let i = 0; - for (const index in val) { - if ( - deepEqual(val[index], lastpredicate[index]) - ) { - continue; - } - const part = closure(val[index], index, val); - elm.replaceChild(part, __elms[i]); - __elms[i] = part; - - i += 1; - } - lastpredicate = Object.keys( - JSON.parse(JSON.stringify(val)), - ); - } else { - for (const part of __elms) { - part.remove(); - } - for (const index in val) { - const value = val[index]; - - const part = closure(value, index, val); - if (part instanceof HTMLElement) { - __elms.push(part); - elm.appendChild(part); - } - } - - lastpredicate = []; - } - }); - } else { - for (const index in predicate) { - const value = predicate[index]; - - const part = closure(value, index, predicate); - if (part instanceof Node) elm.appendChild(part); - - } - } - - delete props["for"]; - delete props["do"]; - } - - - // insert an element at the end - useProp("after", callback => { - JSXAddChild(callback()); - }) - - for (const name in props) { - const references = props[name]; - if (isAJSReferences(references) && name.startsWith("bind:")) { - let reference = references[references.length - 1]; - const propname = name.substring(5); - if (propname == "this") { - reference.proxy[reference.property] = elm; - } else if (propname == "value") { - handle(references, value => elm.value = value); - elm.addEventListener("change", () => { - reference.proxy[reference.property] = elm.value; - }) - } else if (propname == "checked") { - handle(references, value => elm.checked = value); - elm.addEventListener("click", () => { - reference.proxy[reference.property] = elm.checked; - }) - } - delete props[name]; - } - } - - useProp("class", classlist => { - if (typeof classlist === "string") { - elm.className = classlist; - return; - } - - if (isAJSReferences(classlist)) { - handle(classlist, classname => elm.className = classname); - return; - } - - for (const name of classlist) { - if (isAJSReferences(name)) { - let oldvalue = null; - handle(name, value => { - if (typeof oldvalue === "string") { - elm.classList.remove(oldvalue); - } - elm.classList.add(value); - oldvalue = value; - }); - } else { - elm.classList.add(name); - } - } - }); - - // apply the non-reactive properties - for (const name in props) { - const prop = props[name]; - if (isAJSReferences(prop)) { - handle(prop, (val) => { - JSXAddAttributes(elm, name, val); - }); - } else { - JSXAddAttributes(elm, name, prop); - } - } - - return elm; -} - -// glue for nested children -function JSXAddChild(child, cb) { - if (isAJSReferences(child)) { - let appended = []; - handle(child, (val) => { - if (appended.length > 1) { - // this is why we don't encourage arrays (jank) - appended.forEach(n => n.remove()); - appended = JSXAddChild(val, cb); - } else if (appended.length > 0) { - let old = appended[0]; - appended = JSXAddChild(val, cb); - if (appended[0]) { - old.replaceWith(appended[0]) - } else { - old.remove(); - } - } else { - appended = JSXAddChild(val, cb); - } - }); - } else if (child instanceof Node) { - cb(child); - return [child]; - } else if (child instanceof Array) { - let elms = []; - for (const childchild of child) { - elms = elms.concat(JSXAddChild(childchild, cb)); - } - return elms; - } else { - let node = document.createTextNode(child); - cb(node); - return [node]; - } -} - -// Where properties are assigned to elements, and where the *non-reactive* syntax sugar goes -function JSXAddAttributes(elm, name, prop) { - if (typeof prop === "function" && name === "mount") { - prop(elm); - return; - } - - if (typeof prop === "function" && name.startsWith("on:")) { - const names = name.substring(3); - for (const name of names.split("$")) { - elm.addEventListener(name, (...args) => { - window.$el = elm; - prop(...args); - }); - } - return; - } - - if (typeof prop === "function" && name.startsWith("observe")) { - const observerclass = window[`${name.substring(8)}Observer`]; - if (!observerclass) { - console.error(`Observer ${name} does not exist`); - return; - } - const observer = new observerclass(entries => { - for (const entry of entries) { - window.$el = elm; - prop(entry); - } - }); - observer.observe(elm); - return; - } - - elm.setAttribute(name, prop); -} - -function scopify_css(uid, css) { - const virtualDoc = document.implementation.createHTMLDocument(""); - const virtualStyleElement = document.createElement("style"); - virtualDoc.body.appendChild(virtualStyleElement); - - let cssParsed = ""; - - virtualStyleElement.textContent = css; - - //@ts-ignore - for (const rule of virtualStyleElement.sheet.cssRules) { - rule.selectorText = rule.selectorText.includes("self") - ? `.${uid}.self${rule.selectorText.replace("self", "")}` - : `.${uid} ${rule.selectorText}`; - cssParsed += `${rule.cssText}\n`; - } - - return cssParsed; -} -function tagcss(strings, values, isblock) { - const uid = `dream-${Array(16) - .fill(0) - .map(() => { - return Math.floor(Math.random() * 16).toString(16); - }) - .join("")}`; - - const styleElement = document.createElement("style"); - - document.head.appendChild(styleElement); - - const flattened_template = []; - for (const i in strings) { - flattened_template.push(strings[i]); - if (values[i]) { - const prop = values[i]; - - if (isAJSReferences(prop)) { - const current_i = flattened_template.length; - let oldparsed; - handle(prop, (val) => { - flattened_template[current_i] = String(val); - let parsed = flattened_template.join(""); - if (parsed != oldparsed) - if (isblock) - styleElement.textContent = scopify_css( - uid, - parsed, - ); - else - styleElement.textContent = `.${uid} { ${parsed}; }` - oldparsed = parsed; - }); - } else { - flattened_template.push(String(prop)); - } - } - } - - if (isblock) { - styleElement.textContent = scopify_css( - uid, - flattened_template.join(""), - ); - } else { - styleElement.textContent = `.${uid} { ${flattened_template.join("")}; }` - } - - return uid; -} -export function rule(strings, ...values) { - return tagcss(strings, values, false) -} -export function css(strings, ...values) { - return tagcss(strings, values, true); -} - -export function html(strings, ...values) { - let flattened = ""; - let markers = {}; - for (const i in strings) { - let string = strings[i]; - let value = values[i]; - - flattened += string; - if (i < values.length) { - let dupe = Object.values(markers).findIndex(v => v == value); - if (dupe !== -1) { - flattened += Object.keys(markers)[dupe]; - } else { - let marker = "m" + Array(16).fill(0).map(() => Math.floor(Math.random() * 16).toString(16)).join(""); - markers[marker] = value; - flattened += marker; - } - } - } - let dom = new DOMParser().parseFromString(flattened, "text/html"); - if (dom.body.children.length !== 1) - throw "html builder needs exactly one child"; - - function wraph(elm) { - let nodename = elm.nodeName.toLowerCase(); - if (nodename === "#text") - return elm.textContent; - if (nodename in markers) - nodename = markers[nodename]; - - let children = [...elm.childNodes].map(wraph); - for (let i = 0; i < children.length; i++) { - let text = children[i]; - if (typeof text !== "string") continue; - for (const [marker, value] of Object.entries(markers)) { - if (!text) break; - if (!text.includes(marker)) continue; - let before; - [before, text] = text.split(marker); - children = [ - ...children.slice(0, i), - before, - value, - text, - ...children.slice(i + 1) - ]; - i += 2; - } - } - - let attributes = {}; - for (const attr of [...elm.attributes]) { - let val = attr.nodeValue; - if (val in markers) - val = markers[val]; - attributes[attr.name] = val; - } - - return h(nodename, attributes, children); - } - - return wraph(dom.body.children[0]); -} - -function deepEqual(object1, object2) { - const keys1 = Object.keys(object1); - const keys2 = Object.keys(object2); - - if (keys1.length !== keys2.length) { - return false; - } - - for (const key of keys1) { - const val1 = object1[key]; - const val2 = object2[key]; - const areObjects = isObject(val1) && isObject(val2); - if ( - (areObjects && !deepEqual(val1, val2)) || - (!areObjects && val1 !== val2) - ) { - return false; - } - } - - return true; -} - -function isObject(object) { - return object != null && typeof object === "object"; -} +export * from "./js"; +export * from "./css"; +export * from "./html"; diff --git a/css.js b/css.js new file mode 100644 index 0000000..d89e16b --- /dev/null +++ b/css.js @@ -0,0 +1,79 @@ +Object.assign(window, { css, rule, styled: { new: css, rule: rule } }); + +function scopify_css(uid, css) { + const virtualDoc = document.implementation.createHTMLDocument(""); + const virtualStyleElement = document.createElement("style"); + virtualDoc.body.appendChild(virtualStyleElement); + + let cssParsed = ""; + + virtualStyleElement.textContent = css; + + //@ts-ignore + for (const rule of virtualStyleElement.sheet.cssRules) { + rule.selectorText = rule.selectorText.includes("self") + ? `.${uid}.self${rule.selectorText.replace("self", "")}` + : `.${uid} ${rule.selectorText}`; + cssParsed += `${rule.cssText}\n`; + } + + return cssParsed; +} +function tagcss(strings, values, isblock) { + const uid = `dream-${Array(16) + .fill(0) + .map(() => { + return Math.floor(Math.random() * 16).toString(16); + }) + .join("")}`; + + const styleElement = document.createElement("style"); + + document.head.appendChild(styleElement); + + const flattened_template = []; + for (const i in strings) { + flattened_template.push(strings[i]); + if (values[i]) { + const prop = values[i]; + + if (isAJSReferences(prop)) { + const current_i = flattened_template.length; + let oldparsed; + handle(prop, (val) => { + flattened_template[current_i] = String(val); + let parsed = flattened_template.join(""); + if (parsed != oldparsed) + if (isblock) + styleElement.textContent = scopify_css( + uid, + parsed, + ); + else + styleElement.textContent = `.${uid} { ${parsed}; }` + oldparsed = parsed; + }); + } else { + flattened_template.push(String(prop)); + } + } + } + + if (isblock) { + styleElement.textContent = scopify_css( + uid, + flattened_template.join(""), + ); + } else { + styleElement.textContent = `.${uid} { ${flattened_template.join("")}; }` + } + + return uid; +} +export function rule(strings, ...values) { + return tagcss(strings, values, false) +} +export function css(strings, ...values) { + return tagcss(strings, values, true); +} + diff --git a/csspost.js b/csspost.js new file mode 100644 index 0000000..5354143 --- /dev/null +++ b/csspost.js @@ -0,0 +1,2 @@ +(()=>{Object.assign(window,{css:f,rule:d,styled:{new:f,rule:d}});function u(t,o){let r=document.implementation.createHTMLDocument(""),n=document.createElement("style");r.body.appendChild(n);let s="";n.textContent=o;for(let e of n.sheet.cssRules)e.selectorText=e.selectorText.includes("self")?`.${t}.self${e.selectorText.replace("self","")}`:`.${t} ${e.selectorText}`,s+=`${e.cssText} +`;return s}function p(t,o,r){let n=`dream-${Array(16).fill(0).map(()=>Math.floor(Math.random()*16).toString(16)).join("")}`,s=document.createElement("style");document.head.appendChild(s);let e=[];for(let c in t)if(e.push(t[c]),o[c]){let i=o[c];if(isAJSReferences(i)){let m=e.length,a;handle(i,x=>{e[m]=String(x);let l=e.join("");l!=a&&(r?s.textContent=u(n,l):s.textContent=`.${n} { ${l}; }`),a=l})}else e.push(String(i))}return r?s.textContent=u(n,e.join("")):s.textContent=`.${n} { ${e.join("")}; }`,n}function d(t,...o){return p(t,o,!1)}function f(t,...o){return p(t,o,!0)}})(); diff --git a/html.js b/html.js new file mode 100644 index 0000000..e1e6d7c --- /dev/null +++ b/html.js @@ -0,0 +1,65 @@ + +export function html(strings, ...values) { + let flattened = ""; + let markers = {}; + for (const i in strings) { + let string = strings[i]; + let value = values[i]; + + flattened += string; + if (i < values.length) { + let dupe = Object.values(markers).findIndex(v => v == value); + if (dupe !== -1) { + flattened += Object.keys(markers)[dupe]; + } else { + let marker = "m" + Array(16).fill(0).map(() => Math.floor(Math.random() * 16).toString(16)).join(""); + markers[marker] = value; + flattened += marker; + } + } + } + let dom = new DOMParser().parseFromString(flattened, "text/html"); + if (dom.body.children.length !== 1) + throw "html builder needs exactly one child"; + + function wraph(elm) { + let nodename = elm.nodeName.toLowerCase(); + if (nodename === "#text") + return elm.textContent; + if (nodename in markers) + nodename = markers[nodename]; + + let children = [...elm.childNodes].map(wraph); + for (let i = 0; i < children.length; i++) { + let text = children[i]; + if (typeof text !== "string") continue; + for (const [marker, value] of Object.entries(markers)) { + if (!text) break; + if (!text.includes(marker)) continue; + let before; + [before, text] = text.split(marker); + children = [ + ...children.slice(0, i), + before, + value, + text, + ...children.slice(i + 1) + ]; + i += 2; + } + } + + let attributes = {}; + for (const attr of [...elm.attributes]) { + let val = attr.nodeValue; + if (val in markers) + val = markers[val]; + attributes[attr.name] = val; + } + + return h(nodename, attributes, children); + } + + return wraph(dom.body.children[0]); +} + diff --git a/js.js b/js.js new file mode 100644 index 0000000..e51a1a3 --- /dev/null +++ b/js.js @@ -0,0 +1,483 @@ +// The global list of "references", as captured by the proxy in stateful +let __reference_stack = []; + +// We add some extra properties into various objects throughout, better to use symbols and not interfere +let ALICEJS_REFERENCES_MAPPING = Symbol(); +let ALICEJS_REFERENCES_MARKER = Symbol(); +let ALICEJS_STATEFUL_LISTENERS = Symbol(); + +// Say you have some code like +//// let state = stateful({ +//// a: 1 +//// }) +//// let elm =

{window.use(state.a)}

+// +// According to the standard, the order of events is as follows: +// - the getter for window.use gets called, setting __reference_stack to an empty list +// - the proxy for state.a is triggered, pushing { state, "a", Proxy(state) } onto __reference_stack +// - the function that the getter returns is called, popping everything off the stack +// - the JSX factory h() is now passed the *reference* of state.a, not the value +Object.defineProperty(window, "use", { + get: () => { + __reference_stack = []; + return (_sink, mapping) => { + let references = __reference_stack; + __reference_stack = []; + + references[ALICEJS_REFERENCES_MARKER] = true; + if (mapping) references[ALICEJS_REFERENCES_MAPPING] = mapping; + return references; + }; + } +}); +Object.assign(window, { h, stateful, handle, useValue }); + +// This wraps the target in a proxy, doing 2 things: +// - whenever a property is accessed, update the reference stack +// - whenever a property is set, notify the subscribed listeners +// This is what makes our "pass-by-reference" magic work +export function stateful(target) { + target[ALICEJS_STATEFUL_LISTENERS] = []; + + const proxy = new Proxy(target, { + get(target, property, proxy) { + __reference_stack.push({ target, property, proxy }); + return Reflect.get(target, property, proxy); + }, + set(target, property, val) { + for (const listener of target[ALICEJS_STATEFUL_LISTENERS]) { + listener(target, property, val); + } + return Reflect.set(target, property, val); + }, + }); + + return proxy; +} + +export function isAJSReferences(arr) { + return arr instanceof Array && ALICEJS_REFERENCES_MARKER in arr +} + +// This lets you subscribe to a stateful object +export function handle(references, callback) { + if (!isAJSReferences(references)) + throw new Error("Not an AliceJS reference set!"); + + if (ALICEJS_REFERENCES_MAPPING in references) { + const mapping = references[ALICEJS_REFERENCES_MAPPING]; + const used_props = []; + const used_targets = []; + + const values = new Map(); + + const pairs = []; + + const partial_update = (target, prop, val) => { + if (used_props.includes(prop) && used_targets.includes(target)) { + values.get(target)[prop] = val; + } + }; + + const full_update = () => { + const flattened_values = pairs.map( + (pair) => values.get(pair[0])[pair[1]], + ); + + const value = mapping(...flattened_values.reverse()); + + callback(value); + }; + + for (const p of references) { + const target = p.target; + const prop = p.property; + + used_props.push(prop); + used_targets.push(target); + + pairs.push([target, prop]); + + if (!values.has(target)) { + values.set(target, {}); + } + + partial_update(target, prop, target[prop]); + + target[ALICEJS_STATEFUL_LISTENERS].push((t, p, v) => { + partial_update(t, p, v); + full_update(); + }); + } + full_update(); + } else { + const reference = references[references.length - 1]; + const subscription = (target, prop, val) => { + if (prop === reference.property && target === reference.target) { + callback(val); + } + }; + reference.target[ALICEJS_STATEFUL_LISTENERS].push(subscription); + subscription(reference.target, reference.property, reference.target[reference.property]); + } +} + +export function useValue(references) { + let reference = references[references.length - 1]; + return reference.proxy[reference.property]; +} + +// Actual JSX factory. Responsible for creating the HTML elements and all of the *reactive* syntactic sugar +export function h(type, props, ...children) { + if (typeof type === "function") { + let newthis = stateful(Object.create(type.prototype)); + + for (const name in props) { + const references = props[name]; + if (isAJSReferences(references) && name.startsWith("bind:")) { + let reference = references[references.length - 1]; + const propname = name.substring(5); + if (propname == "this") { + reference.proxy[reference.property] = newthis; + } else { + // component two way data binding!! (exact same behavior as svelte:bind) + let isRecursive = false; + + handle(references, value => { + if (isRecursive) { + isRecursive = false; + return; + } + isRecursive = true; + newthis[propname] = value + }); + handle(use(newthis[propname]), value => { + if (isRecursive) { + isRecursive = false; + return; + } + isRecursive = true; + reference.proxy[reference.property] = value; + }); + } + delete props[name]; + } + } + Object.assign(newthis, props); + + let slot = []; + for (const child of children) { + JSXAddChild(child, slot.push.bind(slot)); + } + + let elm = type.apply(newthis, [slot]); + elm.$ = newthis; + newthis.root = elm; + if (newthis.css) { + elm.classList.add(newthis.css); + elm.classList.add("self"); + } + return elm; + } + + + const elm = document.createElement(type); + + for (const child of children) { + JSXAddChild(child, elm.appendChild.bind(elm)); + } + + if (!props) return elm; + + function useProp(name, callback) { + if (!(name in props)) return; + let prop = props[name]; + callback(prop); + delete props[name]; + } + + // insert an element at the start + useProp("before", callback => { + JSXAddChild(callback()); + }) + + // if/then/else syntax + useProp("if", condition => { + let thenblock = props["then"]; + let elseblock = props["else"]; + + if (isAJSReferences(condition)) { + if (thenblock) elm.appendChild(thenblock); + if (elseblock) elm.appendChild(elseblock); + + handle(condition, val => { + if (thenblock) { + if (val) { + thenblock.style.display = ""; + if (elseblock) elseblock.style.display = "none"; + } else { + thenblock.style.display = "none"; + + if (elseblock) elseblock.style.display = ""; + } + } else { + if (val) { + elm.style.display = ""; + } else { + elm.style.display = "none"; + } + } + }); + } else { + if (thenblock) { + if (condition) { + elm.appendChild(thenblock); + } else if (elseblock) { + elm.appendChild(elseblock); + } + } else { + if (condition) { + elm.appendChild(thenblock); + } else if (elseblock) { + elm.appendChild(elseblock); + } else { + elm.style.display = "none"; + return document.createTextNode(""); + } + } + } + + delete props["then"]; + delete props["else"]; + }); + + if ("for" in props && "do" in props) { + const predicate = props["for"]; + const closure = props["do"]; + + if (isAJSReferences(predicate)) { + const __elms = []; + let lastpredicate = []; + handle(predicate, val => { + if ( + Object.keys(val).length && + Object.keys(val).length == lastpredicate.length + ) { + let i = 0; + for (const index in val) { + if ( + deepEqual(val[index], lastpredicate[index]) + ) { + continue; + } + const part = closure(val[index], index, val); + elm.replaceChild(part, __elms[i]); + __elms[i] = part; + + i += 1; + } + lastpredicate = Object.keys( + JSON.parse(JSON.stringify(val)), + ); + } else { + for (const part of __elms) { + part.remove(); + } + for (const index in val) { + const value = val[index]; + + const part = closure(value, index, val); + if (part instanceof HTMLElement) { + __elms.push(part); + elm.appendChild(part); + } + } + + lastpredicate = []; + } + }); + } else { + for (const index in predicate) { + const value = predicate[index]; + + const part = closure(value, index, predicate); + if (part instanceof Node) elm.appendChild(part); + + } + } + + delete props["for"]; + delete props["do"]; + } + + + // insert an element at the end + useProp("after", callback => { + JSXAddChild(callback()); + }) + + for (const name in props) { + const references = props[name]; + if (isAJSReferences(references) && name.startsWith("bind:")) { + let reference = references[references.length - 1]; + const propname = name.substring(5); + if (propname == "this") { + reference.proxy[reference.property] = elm; + } else if (propname == "value") { + handle(references, value => elm.value = value); + elm.addEventListener("change", () => { + reference.proxy[reference.property] = elm.value; + }) + } else if (propname == "checked") { + handle(references, value => elm.checked = value); + elm.addEventListener("click", () => { + reference.proxy[reference.property] = elm.checked; + }) + } + delete props[name]; + } + } + + useProp("class", classlist => { + if (typeof classlist === "string") { + elm.className = classlist; + return; + } + + if (isAJSReferences(classlist)) { + handle(classlist, classname => elm.className = classname); + return; + } + + for (const name of classlist) { + if (isAJSReferences(name)) { + let oldvalue = null; + handle(name, value => { + if (typeof oldvalue === "string") { + elm.classList.remove(oldvalue); + } + elm.classList.add(value); + oldvalue = value; + }); + } else { + elm.classList.add(name); + } + } + }); + + // apply the non-reactive properties + for (const name in props) { + const prop = props[name]; + if (isAJSReferences(prop)) { + handle(prop, (val) => { + JSXAddAttributes(elm, name, val); + }); + } else { + JSXAddAttributes(elm, name, prop); + } + } + + return elm; +} + +// glue for nested children +function JSXAddChild(child, cb) { + if (isAJSReferences(child)) { + let appended = []; + handle(child, (val) => { + if (appended.length > 1) { + // this is why we don't encourage arrays (jank) + appended.forEach(n => n.remove()); + appended = JSXAddChild(val, cb); + } else if (appended.length > 0) { + let old = appended[0]; + appended = JSXAddChild(val, cb); + if (appended[0]) { + old.replaceWith(appended[0]) + } else { + old.remove(); + } + } else { + appended = JSXAddChild(val, cb); + } + }); + } else if (child instanceof Node) { + cb(child); + return [child]; + } else if (child instanceof Array) { + let elms = []; + for (const childchild of child) { + elms = elms.concat(JSXAddChild(childchild, cb)); + } + return elms; + } else { + let node = document.createTextNode(child); + cb(node); + return [node]; + } +} + +// Where properties are assigned to elements, and where the *non-reactive* syntax sugar goes +function JSXAddAttributes(elm, name, prop) { + if (typeof prop === "function" && name === "mount") { + window.$el = elm; + prop(elm); + return; + } + + if (typeof prop === "function" && name.startsWith("on:")) { + const names = name.substring(3); + for (const name of names.split("$")) { + elm.addEventListener(name, (...args) => { + window.$el = elm; + prop(...args); + }); + } + return; + } + + if (typeof prop === "function" && name.startsWith("observe")) { + const observerclass = window[`${name.substring(8)}Observer`]; + if (!observerclass) { + console.error(`Observer ${name} does not exist`); + return; + } + const observer = new observerclass(entries => { + for (const entry of entries) { + window.$el = elm; + prop(entry); + } + }); + observer.observe(elm); + return; + } + + elm.setAttribute(name, prop); +} + +function deepEqual(object1, object2) { + const keys1 = Object.keys(object1); + const keys2 = Object.keys(object2); + + if (keys1.length !== keys2.length) { + return false; + } + + for (const key of keys1) { + const val1 = object1[key]; + const val2 = object2[key]; + const areObjects = isObject(val1) && isObject(val2); + if ( + (areObjects && !deepEqual(val1, val2)) || + (!areObjects && val1 !== val2) + ) { + return false; + } + } + + return true; +} + +function isObject(object) { + return object != null && typeof object === "object"; +} diff --git a/package.json b/package.json index 7cd9460..dd5d220 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.3.1", "description": "A utilitarian HTML rendering library", "scripts": { - "build": "esbuild --minify --bundle AliceJS.js --outfile=index.js && tsc", + "build": "esbuild --minify --bundle AliceJS.js --outfile=index.js && esbuild --minify --bundle css.js js.js html.js --outdir=dist/ && tsc", "watch": "esbuild --watch=forever --bundle AliceJS.js --outfile=index.js & tsc --watch" }, "keywords": ["html","jsx","framework","alicejs"],