Add predefined suiweb
This commit is contained in:
parent
8f488f48bd
commit
5ce44a8d93
1 changed files with 363 additions and 68 deletions
415
src/js/SuiWeb.js
415
src/js/SuiWeb.js
|
@ -1,81 +1,376 @@
|
||||||
|
// @ts-nocheck
|
||||||
|
/* eslint-disable eslint-comments/no-unlimited-disable */
|
||||||
|
/* eslint-disable */
|
||||||
/**
|
/**
|
||||||
* Provides component for rendering elements written in SJDON notation.
|
* SuiWeb
|
||||||
|
* Simple User Interface Tool for Web Exercises
|
||||||
|
*
|
||||||
|
* @author bkrt
|
||||||
|
* @version 0.3.4
|
||||||
|
* @date 11.12.2021
|
||||||
|
*
|
||||||
|
* 0.3.4 - only null or undefined qualify as uninitialized state
|
||||||
|
* 0.3.3 - parseSJDON rewritten to use createElement
|
||||||
|
* - flatten children array for props.children in JSX
|
||||||
|
* 0.3.2 - save and restore of active element improved
|
||||||
|
* 0.3.1 - parseSJDON rewritten
|
||||||
|
* 0.3.0 - style property
|
||||||
|
* 0.2.3 - ES6 modules
|
||||||
|
* 0.2.2 - component state and hooks
|
||||||
|
* 0.2.1 - parse SJDON rewritten, function components
|
||||||
|
* 0.2.0 - render single SJDON structures to DOM
|
||||||
|
*
|
||||||
|
* Based on ideas and code from
|
||||||
|
* Rodrigo Pombo: Build your own React
|
||||||
|
* https://pomb.us/build-your-own-react/
|
||||||
|
*
|
||||||
|
* Thanks to Rodrigo Pombo for a great tutorial and for sharing the
|
||||||
|
* code of the Didact library. Didact is a much more sophisticated
|
||||||
|
* re-implementtation of React's basics than the simple SuiWeb.
|
||||||
*/
|
*/
|
||||||
export class SuiWeb
|
|
||||||
|
/* =====================================================================
|
||||||
|
* SJDON - Conversion
|
||||||
|
* =====================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
// parseSJDON: convert SJDON to createElement calls
|
||||||
|
//
|
||||||
|
// note: this function can also help to use React with SJDON syntax
|
||||||
|
//
|
||||||
|
// to simplify calls add something like this to your components:
|
||||||
|
// let s = (data) => reactFromSJDON(data, React.createElement)
|
||||||
|
//
|
||||||
|
function parseSJDON([type, ...rest], create = createElement)
|
||||||
{
|
{
|
||||||
/**
|
const isObj = (obj) => typeof (obj) === 'object' && !Array.isArray(obj)
|
||||||
* Renders the specified {@link data `data`} and appends it to the specified {@link element `element`}.
|
const children = rest.filter(item => !isObj(item))
|
||||||
*
|
const repr = create(type,
|
||||||
* @param {NodeDescriptor} data
|
Object.assign({}, ...rest.filter(isObj)),
|
||||||
* The node to render written in SJDON notation.
|
...children.map(ch => Array.isArray(ch) ? parseSJDON(ch, create) : ch)
|
||||||
*
|
)
|
||||||
* @param {HTMLElement} element
|
repr.sjdon = children
|
||||||
* The element to add the rendered node to.
|
return repr
|
||||||
*
|
}
|
||||||
* @returns {Node}
|
|
||||||
* The resulting element.
|
|
||||||
*/
|
|
||||||
static render(data, element)
|
|
||||||
{
|
|
||||||
if (Array.isArray(data))
|
|
||||||
{
|
|
||||||
let descriptor = data[0];
|
|
||||||
let args = data.slice(1);
|
|
||||||
|
|
||||||
if (typeof descriptor === "function" || typeof descriptor === "string")
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @type {HTMLElement}
|
|
||||||
*/
|
|
||||||
let result;
|
|
||||||
|
|
||||||
if (typeof descriptor === "function")
|
// create an element representation
|
||||||
{
|
//
|
||||||
/**
|
function createElement(type, props, ...children)
|
||||||
* @type {any[]}
|
{
|
||||||
*/
|
return {
|
||||||
let arg = [];
|
type,
|
||||||
|
props: {
|
||||||
|
...props,
|
||||||
|
children: children.flat().map(child =>
|
||||||
|
typeof child === "object"
|
||||||
|
? child
|
||||||
|
: createTextElement(child)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof args[0] === "object" && !Array.isArray(args[0]))
|
|
||||||
|
// create a text element representation
|
||||||
|
//
|
||||||
|
function createTextElement(text)
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type: "TEXT_ELEMENT",
|
||||||
|
props: {
|
||||||
|
nodeValue: text,
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =====================================================================
|
||||||
|
* Render node tree to DOM
|
||||||
|
* =====================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
// global context
|
||||||
|
let contextStore = createStore();
|
||||||
|
|
||||||
|
|
||||||
|
// remove children and render new subtree
|
||||||
|
//
|
||||||
|
function render(element, container)
|
||||||
|
{
|
||||||
|
while (container.firstChild)
|
||||||
{
|
{
|
||||||
arg = [args[0]];
|
container.removeChild(container.lastChild);
|
||||||
args = args.slice(1);
|
}
|
||||||
|
renderInit(element, container, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// render subtree
|
||||||
|
// and call effects after rendering
|
||||||
|
//
|
||||||
|
function renderInit(element, container, n, childIndex)
|
||||||
|
{
|
||||||
|
contextStore("effects", []);
|
||||||
|
|
||||||
|
// save focus and cursor position of active element
|
||||||
|
let [focusSelector, position] = getFocusInput();
|
||||||
|
|
||||||
|
// ** render the element **
|
||||||
|
renderElem(element, container, n, childIndex);
|
||||||
|
|
||||||
|
// restore focus and cursor position of active element
|
||||||
|
setFocusInput(focusSelector, position);
|
||||||
|
|
||||||
|
// run effects
|
||||||
|
contextStore("effects").forEach(fun => fun());
|
||||||
|
contextStore("effects", []);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// render an element
|
||||||
|
// - if it is in SJDON form: parse first
|
||||||
|
// - render function or host component
|
||||||
|
//
|
||||||
|
function renderElem(element, container, n, childIndex)
|
||||||
|
{
|
||||||
|
if (Array.isArray(element))
|
||||||
|
{
|
||||||
|
element = parseSJDON(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
result = /** @type {HTMLElement} */ (SuiWeb.render(descriptor(...arg), element));
|
if (element.type instanceof Function)
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
result = element.ownerDocument.createElement(descriptor);
|
updateFunctionComponent(element, container, n, childIndex);
|
||||||
element.appendChild(result);
|
} else
|
||||||
|
{
|
||||||
|
updateHostComponent(element, container, childIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// function component
|
||||||
|
// - run function to get child node
|
||||||
|
// - render child node
|
||||||
|
//
|
||||||
|
function updateFunctionComponent(element, container, n, childIndex)
|
||||||
|
{
|
||||||
|
// save re-render function to context
|
||||||
|
contextStore("re-render", () => renderInit(element, container, n, n));
|
||||||
|
let children = element.sjdon ?? element.props.children;
|
||||||
|
let node = element.type({ ...element.props, children });
|
||||||
|
renderElem(node, container, n, childIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// host component
|
||||||
|
// - create dom node
|
||||||
|
// - assign properties
|
||||||
|
// - render child nodes
|
||||||
|
// - add host to dom
|
||||||
|
//
|
||||||
|
function updateHostComponent(element, container, childIndex)
|
||||||
|
{
|
||||||
|
|
||||||
|
// create DOM node
|
||||||
|
const dom =
|
||||||
|
element.type == "TEXT_ELEMENT"
|
||||||
|
? document.createTextNode("")
|
||||||
|
: document.createElement(element.type)
|
||||||
|
|
||||||
|
// assign the element props
|
||||||
|
const isProperty = key => key !== "children"
|
||||||
|
Object.keys(element.props)
|
||||||
|
.filter(isProperty)
|
||||||
|
.forEach(name =>
|
||||||
|
{
|
||||||
|
if (name == "style")
|
||||||
|
{
|
||||||
|
updateStyleAttribute(dom, element.props.style);
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
dom[name] = element.props[name];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// render children
|
||||||
|
element.props.children.forEach((child, index) =>
|
||||||
|
{
|
||||||
|
renderElem(child, dom, index);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof (childIndex) == 'number')
|
||||||
|
{
|
||||||
|
// re-render: replace node
|
||||||
|
container.replaceChild(dom, container.childNodes[childIndex]);
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
// add node to container
|
||||||
|
container.appendChild(dom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// update style attribute, value can be:
|
||||||
|
// - a CSS string: set to style attribute
|
||||||
|
// - an object: merged to style attribute
|
||||||
|
// - an array of objects: merged to style attribute
|
||||||
|
//
|
||||||
|
function updateStyleAttribute(dom, styles)
|
||||||
|
{
|
||||||
|
if (typeof (styles) == "string")
|
||||||
|
{
|
||||||
|
dom.style = styles;
|
||||||
|
} else if (Array.isArray(styles))
|
||||||
|
{
|
||||||
|
Object.assign(dom.style, ...styles);
|
||||||
|
} else if (typeof (styles) == "object")
|
||||||
|
{
|
||||||
|
Object.assign(dom.style, styles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* =====================================================================
|
||||||
|
* Handling state
|
||||||
|
* =====================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
// element state
|
||||||
|
let stateHooks = createStore();
|
||||||
|
|
||||||
|
|
||||||
|
// state hook
|
||||||
|
// - access state via id and key
|
||||||
|
// - return state and update function
|
||||||
|
//
|
||||||
|
function useState(id, key, init)
|
||||||
|
{
|
||||||
|
let idKey = "id:" + id + "-key:" + key;
|
||||||
|
|
||||||
|
// prepare render function
|
||||||
|
let renderFunc = contextStore("re-render");
|
||||||
|
|
||||||
|
// define function to update state
|
||||||
|
function updateValue(updateFun, rerender = true)
|
||||||
|
{
|
||||||
|
stateHooks(idKey, updateFun(stateHooks(idKey)));
|
||||||
|
if (rerender) renderFunc();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let arg of args)
|
// new state: set initial value
|
||||||
|
if ([undefined, null].includes(stateHooks(idKey)))
|
||||||
{
|
{
|
||||||
if (typeof arg === "object" && !Array.isArray(arg))
|
stateHooks(idKey, init);
|
||||||
{
|
|
||||||
Object.assign(result, arg);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
SuiWeb.render(arg, result);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return [stateHooks(idKey), updateValue];
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
|
||||||
|
// effect hook
|
||||||
|
// add function to effects array
|
||||||
|
//
|
||||||
|
function useEffect(fun)
|
||||||
|
{
|
||||||
|
contextStore("effects", [...contextStore("effects"), fun]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// create a key-value-store
|
||||||
|
// return accessor function
|
||||||
|
//
|
||||||
|
function createStore()
|
||||||
|
{
|
||||||
|
let data = {};
|
||||||
|
function access(key, ...value)
|
||||||
{
|
{
|
||||||
throw new SyntaxError();
|
if (value.length === 0)
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (typeof data === "string")
|
|
||||||
{
|
{
|
||||||
return element.appendChild(element.ownerDocument.createTextNode(data));
|
return data[key];
|
||||||
}
|
} else
|
||||||
else
|
|
||||||
{
|
{
|
||||||
throw new SyntaxError();
|
data[key] = value[0];
|
||||||
|
return value[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return access;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* =====================================================================
|
||||||
|
* Get and set focus and position in certain elements
|
||||||
|
* Note: this is a quick&dirty solution that probably fails when
|
||||||
|
* elements are added or removed
|
||||||
|
* =====================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
// create a CSS selector for a given node
|
||||||
|
//
|
||||||
|
function getSelectorOf(node)
|
||||||
|
{
|
||||||
|
let selector = ""
|
||||||
|
while (node != document.body)
|
||||||
|
{
|
||||||
|
if (selector != "") selector = ">" + selector
|
||||||
|
|
||||||
|
if (node.id)
|
||||||
|
{
|
||||||
|
selector = "#" + node.id + selector
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = Array.from(node.parentNode.children)
|
||||||
|
.filter(item => item.tagName == node.tagName)
|
||||||
|
.findIndex(item => item == node)
|
||||||
|
|
||||||
|
selector = node.tagName + ":nth-of-type(" + (index + 1) + ")" + selector
|
||||||
|
node = node.parentNode
|
||||||
|
}
|
||||||
|
return selector
|
||||||
|
}
|
||||||
|
|
||||||
|
// find a selector for the element that has focus and the cursor
|
||||||
|
// position in the element
|
||||||
|
//
|
||||||
|
function getFocusInput()
|
||||||
|
{
|
||||||
|
const active = document.activeElement;
|
||||||
|
let sel = active ? getSelectorOf(active) : undefined
|
||||||
|
let position = active ? active.selectionStart : undefined;
|
||||||
|
return [sel, position];
|
||||||
|
}
|
||||||
|
|
||||||
|
// set focus to an element in a list of elements matching a
|
||||||
|
// selector and position cursor in the element
|
||||||
|
//
|
||||||
|
function setFocusInput(selector, position)
|
||||||
|
{
|
||||||
|
if (selector && typeof (selector) == 'string')
|
||||||
|
{
|
||||||
|
console.log("Sel:" + selector)
|
||||||
|
let el = document.querySelector(selector);
|
||||||
|
if (el) el.focus();
|
||||||
|
if (el && "selectionStart" in el
|
||||||
|
&& "selectionEnd" in el
|
||||||
|
&& position !== undefined)
|
||||||
|
{
|
||||||
|
el.selectionStart = position;
|
||||||
|
el.selectionEnd = position;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* =====================================================================
|
||||||
|
* Module export
|
||||||
|
* =====================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { render, createElement, useState, useEffect };
|
||||||
|
|
||||||
|
|
||||||
|
/* =====================================================================
|
||||||
|
* EOF
|
||||||
|
* =====================================================================
|
||||||
|
*/
|
||||||
|
|
Loading…
Reference in a new issue