NEWS: Welcome to my new homepage! <3

Added picker example - figure - Unnamed repository; edit this file 'description' to name the repository.

figure

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 71bcd6376c673c2b386604e00d1e55a030d892b3
parent f07d629d85be64a3a4ce2166b5d45b5b7715d0f1
Author: typable <contact@typable.dev>
Date:   Fri, 16 Sep 2022 23:55:51 +0200

Added picker example

Diffstat:
Dexample.html | 105-------------------------------------------------------------------------------
Aexamples/picker.html | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dlib.js | 123-------------------------------------------------------------------------------
Atlx.js | 138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 230 insertions(+), 228 deletions(-)

diff --git a/example.html b/example.html @@ -1,105 +0,0 @@ -<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter&family=Noto+Sans"> - -<div id="app"></div> - -<style> - - * { - font-family: 'Noto Sans', sans-serif; - } - - body { - margin: 0; - } - -</style> - -<script type="module"> - - import {useState, createElement} from 'https://cdn.skypack.dev/react'; - import {render} from 'https://cdn.skypack.dev/react-dom'; - import {createTlx, css} from './lib.js'; - - const tlx = createTlx(createElement); - - const Button = (props, $slot) => { - const styles = { - ...css` - min-width: 40px; - height: 40px; - border: none; - outline: none; - border: 1px solid #a3a3a3; - background-color: ${props.disabled ? '#f5f5f5' : '#e5e5e5'}; - cursor: ${props.disabled ? 'default' : 'pointer'}; - font-size: 1rem; - `, - ...props.styles - }; - return tlx` - <button - disabled="${props.disabled}" - style="${styles}" - @click="${() => props.onClick()}" - > - ${$slot} - </button> - `; - } - - const Counter = (props) => { - const [defaultValue, setDefaultValue] = useState(0); - const { min = 0, max = 999, value = defaultValue, setValue = setDefaultValue } = props ?? {}; - const validate = (value) => { - if(/^\d+$/.test(value) && parseInt(value) <= max) { - setValue(parseInt(value)); - } - } - return tlx` - <div style="display: inline-flex; width: 100%; border: 1px solid #a3a3a3; border-radius: 4px; overflow: hidden; height: 40px; box-sizing: border-box;"> - ${Button({ - onClick: () => setValue(value - 1), - styles: css` - font-size: 1.2rem; - border-width: 0px 1px 0px 0px; - `, - disabled: value <= min - }, '-')} - <input type="text" @input="${(event) => validate(event.target.value)}" value="${value}" style="font-size: 1rem; text-align: center; border: none; flex: 1; min-width: 0; height: 40px; outline: none;"> - ${Button({ - onClick: () => setValue(value + 1), - styles: css` - font-size: 1.2rem; - border-width: 0px 0px 0px 1px; - `, - disabled: value >= max - }, '+')} - </div> - `; - } - - const App = () => { - const [quantity, setQuantity] = useState(1); - const isDisabled = quantity <= 0; - return tlx` - <div style="margin: 100px; display: flex; width: 150px; flex-direction: column; gap: 15px; align-items: flex-start;"> - ${Counter({ value: quantity, setValue: setQuantity })} - ${Button({ - styles: css` - background-color: ${isDisabled ? '#f5f5f5' : '#f97316'}; - border-color: ${isDisabled ? '#d4d4d4' : '#c2410c'}; - color: ${isDisabled ? '#a3a3a3' : '#ffffff'}; - border-radius: 4px; - width: 100%; - `, - onClick: () => alert(`Added ${quantity} ${quantity > 1 ? 'items' : 'item'} to the cart`), - disabled: isDisabled - }, 'Add to cart')} - </div> - `; - } - - const app = document.querySelector('#app'); - render(createElement(App), app); - -</script> diff --git a/examples/picker.html b/examples/picker.html @@ -0,0 +1,92 @@ +<div id="app"></div> + +<script type="module"> + + import {useState, createElement} from 'https://cdn.skypack.dev/react'; + import {render} from 'https://cdn.skypack.dev/react-dom'; + import {createTlx, css} from '../tlx.js'; + + const tlx = createTlx(createElement); + + const Range = ({index, value, setValue}) => { + const style = css` + .range input[type=range] { + width: 100%; + -webkit-appearance: none; + } + + .range.v-${index}-${value} input[type=range]::-webkit-slider-runnable-track { + background-color: rgb(${Array.from([0, 0, 0], (item, i) => i === index ? value : item).join(', ')}); + width: 100%; + height: 12px; + } + + .range input[type=range]::-webkit-slider-thumb { + background-color: #CCC; + width: 12px; + height: 12px; + -webkit-appearance: none; + cursor: pointer; + } + `; + + return tlx` + <div class="range v-${index}-${value}"> + ${style} + <input + type="range" + @change="${(event) => setValue(Number(event.target.value))}" + min="0" + max="255" + value="${value}" + > + </div> + `; + } + + const Picker = () => { + const [red, setRed] = useState(255); + const [green, setGreen] = useState(65); + const [blue, setBlue] = useState(155); + + const style = css` + .picker { + display: inline-flex; + flex-direction: column; + gap: 10px; + padding: 50px; + } + + .picker .preview { + width: 200px; + height: 200px; + background-color: rgb(${red}, ${green}, ${blue}); + } + + .picker p { + margin: 0px; + text-align: center; + font-family: monospace; + font-size: 16px; + } + `; + + const hex = (dec) => dec.toString(16).toUpperCase().padStart(2, '0'); + + return tlx` + <div class="picker"> + ${style} + <div class="preview"></div> + <p>rgb(${red}, ${green}, ${blue})</p> + <p>#${hex(red)}${hex(green)}${hex(blue)}</p> + ${Range({ index: 0, value: red, setValue: setRed })} + ${Range({ index: 1, value: green, setValue: setGreen })} + ${Range({ index: 2, value: blue, setValue: setBlue })} + </div> + `; + } + + const app = document.querySelector('#app'); + render(createElement(Picker), app); + +</script> diff --git a/lib.js b/lib.js @@ -1,123 +0,0 @@ -const parser = new DOMParser(); -let counter = 1000; - -export const createTlx = (createElement) => { - function tlx(parts, ...props) { - const [html, refs] = ltr(parts, props); - const dom = parser.parseFromString(html, 'text/html'); - if(dom.body.childNodes.length !== 1) { - throw 'invalid DOM structure!'; - } - const node = dom.body.childNodes[0]; - const elements = render(node, refs); - if(elements.length !== 1) { - throw 'invalid VDOM structure!'; - } - return elements[0]; - } - - function render(node, refs) { - if(node.nodeType === Node.TEXT_NODE) { - return apply(node.textContent, refs); - } - const tag = node.tagName.toLowerCase(); - const attributes = {}; - for(const attribute of node.attributes) { - const key = attribute.name; - const value = attribute.textContent; - let isDynamic = false; - for(const ref in refs) { - if(value === ref) { - let match = null; - if((match = /^@(\w+)$/.exec(key)) !== null) { - const event = match[1]; - attributes[`on${event.substr(0, 1).toUpperCase()}${event.substr(1)}`] = refs[ref]; - } - else { - attributes[key] = refs[ref]; - } - isDynamic = true; - break; - } - } - if(!isDynamic) { - if(key === 'style') { - const styles = {}; - for(const item of value.split(';')) { - if(item.trim().length === 0) { - break; - } - let [key, value] = item.split(':'); - key = key.trim(); - let match = null; - if((match = /-(\w)/.exec(key)) !== null) { - const char = match[1]; - key = key.replace(`-${char}`, char.toUpperCase()); - } - value = value.trim(); - styles[key] = apply(value, refs).join(''); - } - attributes[key] = styles; - } - else { - attributes[key] = apply(value, refs).join(''); - } - } - } - const children = []; - (node.childNodes ?? []).forEach((child) => children.push(...render(child, refs))); - return [createElement(tag, attributes, ...children)]; - } - - return tlx; -} - -export const css = (parts, ...props) => { - const [css, refs] = ltr(parts, props); - const styles = {}; - for(const item of css.split(';')) { - if(item.trim().length === 0) { - break; - } - let [key, value] = item.split(':'); - key = key.trim(); - let match = null; - if((match = /-(\w)/.exec(key)) !== null) { - const char = match[1]; - key = key.replace(`-${char}`, char.toUpperCase()); - } - value = value.trim(); - styles[key] = apply(value, refs).join(''); - } - return styles; -} - -function apply(value, refs) { - let values = []; - const expr = /\$tlx-\d+/g; - let match = null; - let last = 0; - while((match = expr.exec(value)) !== null) { - const index = match.index; - values.push(value.substring(last, index)); - values.push(refs[match[0]]); - last = index + match[0].length; - } - values.push(value.substring(last)); - return values; -} - -function ltr(parts, props) { - let string = ''; - const refs = {}; - for(let i = 0; i < parts.length; i++) { - string += parts[i]; - if(props[i] !== undefined) { - const id = `$tlx-${counter}`; - refs[id] = props[i]; - string += id ?? ''; - counter++; - } - } - return [string.trim(), refs]; -} diff --git a/tlx.js b/tlx.js @@ -0,0 +1,138 @@ +const parser = new DOMParser(); +let counter = 1000; +let tlx = null; + +export const createTlx = (createElement) => { + function parse(parts, ...props) { + const [html, refs] = ltr(parts, props); + const dom = parser.parseFromString(html, 'text/html'); + const node = dom.body.childNodes[0] ?? dom.head.childNodes[0]; + const elements = render(node, refs); + if(elements.length !== 1) { + throw 'invalid VDOM structure!'; + } + return elements[0]; + } + + function render(node, refs) { + if(node.nodeType === Node.TEXT_NODE) { + return apply(node.textContent, refs); + } + if(node.nodeType === Node.COMMENT_NODE) { + return []; + } + const tag = node.tagName; + const attributes = {}; + for(const attribute of node.attributes) { + const key = attribute.name; + const value = attribute.textContent; + let isDynamic = false; + for(const ref in refs) { + if(value === ref) { + let match = null; + if((match = /^@(\w+)$/.exec(key)) !== null) { + const event = match[1]; + attributes[`on${event.substr(0, 1).toUpperCase()}${event.substr(1)}`] = refs[ref]; + } + else { + attributes[key] = refs[ref]; + } + isDynamic = true; + break; + } + } + if(!isDynamic) { + if(key === 'style') { + const styles = {}; + for(const item of value.split(';')) { + if(item.trim().length === 0) { + break; + } + let [key, value] = item.split(':'); + key = key.trim(); + let match = null; + if((match = /-(\w)/.exec(key)) !== null) { + const char = match[1]; + key = key.replace(`-${char}`, char.toUpperCase()); + } + value = value.trim(); + styles[key] = apply(value, refs).join(''); + } + attributes[key] = styles; + } + else { + attributes[key] = apply(value, refs).join(''); + } + } + } + const children = []; + (node.childNodes ?? []).forEach((child) => children.push(...render(child, refs))); + return [createElement(tag, attributes, ...children)]; + } + + tlx = parse; + return tlx; +} + +export const css = (parts, ...props) => { + const [css, refs] = ltr(parts, props); + return tlx` + <style type="text/css"> + ${apply(css, refs).join('')} + </style> + `; +} + +export const style = (parts, ...props) => { + const [css, refs] = ltr(parts, props); + const styles = {}; + for(const item of css.split(';')) { + if(item.trim().length === 0) { + break; + } + let [key, value] = item.split(':'); + key = key.trim(); + let match = null; + if((match = /-(\w)/.exec(key)) !== null) { + const char = match[1]; + key = key.replace(`-${char}`, char.toUpperCase()); + } + value = value.trim(); + styles[key] = apply(value, refs).join(''); + } + return styles; +} + +function apply(value, refs) { + let values = []; + const expr = /\$tlx-\d+/g; + let match = null; + let last = 0; + while((match = expr.exec(value)) !== null) { + const index = match.index; + values.push(value.substring(last, index)); + let refValue = refs[match[0]]; + if(refValue instanceof Function) { + refValue = refValue(); + } + values.push(refValue); + last = index + match[0].length; + } + values.push(value.substring(last)); + return values; +} + +function ltr(parts, props) { + let string = ''; + const refs = {}; + for(let i = 0; i < parts.length; i++) { + string += parts[i]; + if(props[i] !== undefined) { + const id = `$tlx-${counter}`; + refs[id] = props[i]; + string += id ?? ''; + counter++; + } + } + return [string.trim(), refs]; +}