lightview
Version:
Small, simple, powerful web UI and micro front end creation ... Great ideas from Svelte, React, Vue and Riot combined.
607 lines (576 loc) • 25.9 kB
JavaScript
const toJSON = (value) => {
if([-Infinity,Infinity].includes(value)) return `@${value}`;
if(typeof(value)==="number" && isNaN(value)) return "@NaN";
if(value && typeof(value)==="object") {
return Object.entries(value)
.reduce((json,[key,value]) => {
if(value && typeof(value)==="object" && value.toJSON) value = value.toJSON();
json[key] = toJSON(value);
return json;
},Array.isArray(value) ? [] : {})
}
return value;
};
function reviver(property,value) {
if(value==="@-Infinity") return -Infinity;
if(value==="@Infinity") return Infinity;
if(value==="@NaN") return NaN;
return value;
}
const deepEqual = (a,b,matchType=deepEqual.LEFT, seen=new Set()) => {
if(matchType===deepEqual.RIGHT) return deepEqual(b,a,deepEqual.LEFT,seen);
if(matchType===deepEqual.COMMUTATIVE) return deepEqual(a,b,deepEqual.LEFT) && deepEqual(b,a,deepEqual.LEFT);
if(a===b) return true;
const type = typeof(a);
if(type==="function" || type!==typeof(b) || (a && !b) || (b && !a)) return false;
if(type==="number" && isNaN(a) && isNaN(b)) return true;
if(a && type==="object") {
if(seen.has(a)) return true;
seen.add(a);
if(a.constructor!==b.constructor || a.length!==b.length || a.size!==b.size) return false;
if(a instanceof Date) a.getTime() === b.getTime();
if(a instanceof RegExp) return a.toString() === b.toString();
if(a instanceof Set) {
for(const avalue of [...a]) {
if(![...b].some((bvalue) => deepEqual(avalue,bvalue,matchType,seen))) return false;
}
return true;
}
if(a instanceof Map) {
for(const [key,value] of [...a]) {
if(!deepEqual(b.get(key),value,matchType,seen)) return false;
}
return true;
}
for(const key in a) {
if(!deepEqual(a[key],b[key],matchType,seen)) return false;
}
return true;
}
return false;
}
deepEqual.LEFT = 1;
deepEqual.COMMUTATIVE = 2;
deepEqual.RIGHT = 3;
function ValidityState(options) {
if(!this || !(this instanceof ValidityState)) return new ValidityState(options);
Object.assign(this,{
valid:false,
badInput:undefined,
customError:undefined,
patternMismatch:undefined,
rangeUnderflow:undefined,
rangeOverflow:undefined,
typeMismatch:undefined,
valueMissing:undefined,
stepMistmatch:undefined,
tooLong:undefined,
tooShort:undefined
},options);
}
function DataType(options) {
if(!this || !(this instanceof DataType)) return new DataType(options);
Object.assign(this,options);
}
DataType.prototype.toJSON = function() {
return toJSON(this);
}
const tryParse = (value) => {
try {
return JSON.parse(value+"",reviver)
} catch(e) {
}
}
const ifInvalid = (variable) => {
variable.validityState.type = typeof(variable.type)==="string" ? variable.type : variable.type.type;
throw new TypeError(JSON.stringify(DataType(variable)));
// or could return existing value variable.value
// or could return nothing
}
const validateAny = function(value,variable) {
if(value===undefined && variable.value===undefined) {
return this.default;
}
if(this.required && value==null) {
variable.validityState = ValidityState({valueMissing: true});
} else {
variable.validityState = ValidityState({valid:true});
return value;
}
return this.whenInvalid(variable,value);
}
const any = ({required=false,whenInvalid = ifInvalid,...rest}) => { // ...rest allows use of property "default", which is otherwise reserved
if(typeof(required)!=="boolean") throw new TypeError(`required, ${JSON.stringify(required)}, must be a boolean`);
if(typeof(whenInvalid)!=="function") throw new TypeError(`whenInvalid, ${whenInvalid}, must be a function`);
return {
type: "any",
required,
whenInvalid,
...rest,
validate: validateAny
}
}
any.validate = validateAny;
any.required = false;
const validateArray = function(value,variable) {
if(value===undefined && variable.value===undefined) {
return this.default;
}
if(this.required && value==null) {
variable.validityState = ValidityState({valueMissing: true});
} else {
let result = this.coerce && typeof(value)==="string" ? tryParse(value.startsWith("[") ? value : `[${value}]`) : value;
if (!Array.isArray(result)) {
if (value.includes(",")) result = value.split(",");
}
if(typeof(result)!=="object" || !(result instanceof Array || Array.isArray(result))) {
variable.validityState = ValidityState({typeMismatch:true,value});
} else if(result.length<this.minlength) {
variable.validityState = ValidityState({tooShort:true,value});
} else if(result.length>this.maxlength) {
variable.validityState = ValidityState({tooLong:true,value});
} else {
variable.validityState = ValidityState({valid:true});
return result;
}
}
return this.whenInvalid(variable,value);
}
const array = ({coerce=false, required = false,whenInvalid = ifInvalid,maxlength=Infinity,minlength=0,...rest}={}) => {
if(typeof(coerce)!=="boolean") throw new TypeError(`coerce, ${JSON.stringify(coerce)}, must be a boolean`);
if(typeof(required)!=="boolean") throw new TypeError(`required, ${JSON.stringify(required)}, must be a boolean`);
if(typeof(whenInvalid)!=="function") throw new TypeError(`whenInvalid, ${whenInvalid}, must be a function`);
if(typeof(maxlength)!=="number") throw new TypeError(`maxlength, ${JSON.stringify(maxlength)}, must be a number`);
if(typeof(minlength)!=="number") throw new TypeError(`minlength, ${JSON.stringify(minlength)}, must be a number`);
if(rest.default!==undefined && (typeof(rest.default)!=="object" || !(rest.default instanceof Array || Array.isArray(rest.default)))) throw new TypeError(`default, ${rest.default}, must be an Array`);
return {
type: "array",
coerce,
required,
whenInvalid,
maxlength,
minlength,
...rest,
validate: validateArray
}
}
const validateBoolean = function(value,variable) {
if(value===undefined && variable.value===undefined) {
return this.default;
}
if(variable.value===undefined) value = this.default;
if(this.required && value==null) {
variable.validityState = ValidityState({valueMissing: true});
} else {
const result = this.coerce ? tryParse(value) : value;
if(typeof(result)!=="boolean") {
variable.validityState = ValidityState({typeMismatch: true, value});
} else {
variable.validityState = ValidityState({valid:true});
return result;
}
}
return this.whenInvalid(variable,value);
}
const boolean = ({coerce=false,required=false, whenInvalid = ifInvalid,...rest}={}) =>{
if(typeof(coerce)!=="boolean") throw new TypeError(`coerce, ${JSON.stringify(coerce)}, must be a boolean`);
if(typeof(required)!=="boolean") throw new TypeError(`required, ${JSON.stringify(required)}, must be a boolean`);
if(typeof(whenInvalid)!=="function") throw new TypeError(`whenInvalid, ${whenInvalid}, must be a function`);
if(rest.default!==undefined && typeof(rest.default)!=="boolean") throw new TypeError(`default, ${rest.default}, must be a boolean`);
return {
type: "boolean",
coerce,
required,
whenInvalid,
...rest,
validate: validateBoolean
}
}
const isDuration = (value) => {
return parseDuration(value)!==undefined;
}
const durationMilliseconds = {
ms: 1,
s: 1000,
m: 1000 * 60,
h: 1000 * 60 * 60,
d: 1000 * 60 * 60 * 24,
w: 1000 * 60 * 60 * 24 * 7,
mo: (1000 * 60 * 60 * 24 * 365.25)/12,
q: (1000 * 60 * 60 * 24 * 365.25)/4,
y: (1000 * 60 * 60 * 24 * 365.25)
}
const parseDuration = (value) => {
if(typeof(value)==="number") return value;
if(typeof(value)==="string") {
const num = parseFloat(value),
suffix = value.substring((num+"").length);
if(typeof(num)==="number" && !isNaN(num) && suffix in durationMilliseconds) {
return durationMilliseconds[suffix] * num;
}
}
return null;
}
const validateDuration = function(value,variable) {
const result = parseDuration(value);
if(result==null && variable.value===undefined) {
return parseDuration(this.default);
}
if(this.required && result==null) {
variable.validityState = ValidityState({valueMissing: true});
} else {
if(typeof(result)!=="number") {
variable.validityState = ValidityState({typeMismatch:true,value});
} else if(isNaN(result)) {
variable.validityState = ValidityState({badInput:true,value});
} else if(this.min!=null && result<parseDuration(this.min)) {
variable.validityState = ValidityState({rangeUnderflow:true,value});
} else if(this.max!=null && result>parseDuration(this.max)) {
variable.validityState = ValidityState({rangeOverflow:true,value});
} else if(this.step!==null && (result % parseDuration(this.step)!==0)) {
variable.validityState = ValidityState({rangeUnderflow:true,value});
} else {
variable.validityState = ValidityState({valid:true});
return result;
}
}
return this.whenInvalid(variable,value);
}
const duration = ({required = false,whenInvalid = ifInvalid,min=-Infinity,max=Infinity,step = 1,...rest}={}) => {
if(typeof(required)!=="boolean") throw new TypeError(`required, ${JSON.stringify(required)}, must be a boolean`);
if(typeof(whenInvalid)!=="function") throw new TypeError(`whenInvalid, ${whenInvalid}, must be a function`);
if(min!=null && !parseDuration(min)) throw new TypeError(`min, ${JSON.stringify(min)}, must be a duration`);
if(max!=null && !parseDuration(max)) throw new TypeError(`max, ${JSON.stringify(max)}, must be a duration`);
if(step!=null && !parseDuration(step)) throw new TypeError(`step, ${JSON.stringify(step)}, must be a duration`);
if(rest.default!==undefined && !parseDuration(rest.default)) throw new TypeError(`default, ${JSON.stringify(rest.default)}, must be a duration`);
return {
type: "duration",
coerce: false,
required,
whenInvalid,
min,
max,
step,
...rest,
validate: validateDuration
}
}
duration.parse = parseDuration;
const validateNumber = function(value,variable) {
if(value===undefined && variable.value===undefined) {
return this.default;
}
if(this.required && value==null) {
variable.validityState = ValidityState({valueMissing: true});
} else {
const result = this.coerce ? tryParse(value) : value;
if(typeof(result)!=="number") {
variable.validityState = ValidityState({typeMismatch:true,value});
} else if(isNaN(result) && !this.allowNaN) {
variable.validityState = ValidityState({badInput:true,value});
} else if(this.min!=null && result<this.min && !(this.min===-Infinity && isNaN(result))) {
variable.validityState = ValidityState({rangeUnderflow:true,value});
} else if(this.max!=null && result>this.max && !(this.max===Infinity && isNaN(result))) {
variable.validityState = ValidityState({rangeOverflow:true,value});
} else if(this.step!=null && (result % this.step)!==0) {
variable.validityState = ValidityState({rangeUnderflow:true,value});
} else {
variable.validityState = ValidityState({valid:true});
return result;
}
}
return this.whenInvalid(variable,value);
}
const number = ({coerce=false,required = false,whenInvalid = ifInvalid,min=-Infinity,max=Infinity,step,allowNaN = true,...rest}={}) => {
if(typeof(coerce)!=="boolean") throw new TypeError(`coerce, ${JSON.stringify(coerce)}, must be a boolean`);
if(typeof(required)!=="boolean") throw new TypeError(`required, ${JSON.stringify(required)}, must be a boolean`);
if(typeof(whenInvalid)!=="function") throw new TypeError(`whenInvalid, ${whenInvalid}, must be a function`);
if(min!=null && typeof(min)!=="number") throw new TypeError(`min, ${JSON.stringify(min)}, must be a number`);
if(max!=null && typeof(max)!=="number") throw new TypeError(`max, ${JSON.stringify(max)}, must be a number`);
if(step!=null && typeof(step)!=="number") throw new TypeError(`step, ${JSON.stringify(step)}, must be a number`);
if(typeof(allowNaN)!=="boolean") throw new TypeError(`step, ${JSON.stringify(allowNaN)}, must be a boolean`);
if(rest.default!==undefined && typeof(rest.default)!=="number") throw new TypeError(`default, ${JSON.stringify(rest.default)}, must be a number`);
return {
type: "number",
coerce,
required,
whenInvalid,
min,
max,
step,
allowNaN,
...rest,
validate: validateNumber
}
}
const validateObject = function(value,variable) {
if(value===undefined && variable.value===undefined) {
return this.default;
}
if(this.required && value==null) {
variable.validityState = ValidityState({valueMissing: true});
} else {
const result = this.coerce ? tryParse(value) : value;
if(typeof(result)!=="object") {
variable.validityState = ValidityState({typeMismatch:true,value});
} else {
variable.validityState = ValidityState({valid:true});
return result;
}
}
return this.whenInvalid(variable,value);
}
const object = ({coerce=false, required = false,whenInvalid = ifInvalid,...rest}={}) => {
if(typeof(coerce)!=="boolean") throw new TypeError(`coerce, ${JSON.stringify(coerce)}, must be a boolean`);
if(typeof(required)!=="boolean") throw new TypeError(`required, ${JSON.stringify(required)}, must be a boolean`);
if(typeof(whenInvalid)!=="function") throw new TypeError(`whenInvalid, ${whenInvalid}, must be a function`);
if(rest.default!==undefined && typeof(rest.default)!=="object") throw new TypeError(`default, ${rest.default}, must be of type object`);
return {
type: "object",
coerce,
required,
whenInvalid,
...rest,
validate: validateObject
}
}
const validateString = function(value,variable) {
if(value===undefined && variable.value===undefined) {
return this.default;
}
if(this.required && value==null) {
variable.validityState = ValidityState({valueMissing: true});
} else {
const result = this.coerce ? value+"" : value;
if(typeof(result)!=="string") {
variable.validityState = ValidityState({typeMismatch:true,value});
} else if(result.length<this.minlength) {
variable.validityState = ValidityState({tooShort:true,value});
} else if(result.length>this.maxlength) {
variable.validityState = ValidityState({tooLong:true,value});
} else {
variable.validityState = ValidityState({valid:true});
return result;
}
}
return this.whenInvalid(variable,value);
}
const string = ({coerce=false, required = false,whenInvalid = ifInvalid, maxlength = Infinity, minlength = 0, pattern,...rest}={}) => {
if(typeof(coerce)!=="boolean") throw new TypeError(`coerce, ${JSON.stringify(coerce)}, must be a boolean`);
if(typeof(required)!=="boolean") throw new TypeError(`required, ${JSON.stringify(required)}, must be a boolean`);
if(typeof(whenInvalid)!=="function") throw new TypeError(`whenInvalid, ${whenInvalid}, must be a function`);
if(typeof(maxlength)!=="number") throw new TypeError(`maxlength, ${JSON.stringify(maxlength)}, must be a number`);
if(typeof(minlength)!=="number") throw new TypeError(`minlength, ${JSON.stringify(minlength)}, must be a number`);
if(pattern && (typeof(pattern)!=="object" || !(pattern instanceof RegExp))) throw new TypeError(`pattern, ${pattern}, must be a RegExp`);
if(rest.default!==undefined && typeof(rest.default)!=="string") throw new TypeError(`default, ${rest.default}, must be a string`);
return {
type: "string",
coerce,
required,
whenInvalid,
maxlength,
minlength,
...rest,
validate: validateString
}
}
const html = (...args) => string(...args);
html.safe = true;
const css = (...args) => string(...args);
css.safe = true;
const script = (...args) => string(...args);
script.safe = true;
const validateSymbol = function(value,variable) {
if(value===undefined && variable.value===undefined) {
return this.default;
}
if(this.required && value==null) {
variable.validityState = ValidityState({valueMissing: true});
} else {
const result = !!(this.coerce && typeof(value)!=="symbol" ? Symbol(value+"") : value);
if(typeof(result)!=="symbol") {
variable.validityState = ValidityState({typeMismatch: true, value});
} else {
variable.validityState = ValidityState({valid:true});
return result;
}
}
return this.whenInvalid(variable,value);
}
const symbol = ({coerce=false,required=false, whenInvalid = ifInvalid,...rest}={}) =>{
if(typeof(coerce)!=="boolean") throw new TypeError(`coerce, ${JSON.stringify(coerce)}, must be a boolean`);
if(typeof(required)!=="boolean") throw new TypeError(`required, ${JSON.stringify(required)}, must be a boolean`);
if(typeof(whenInvalid)!=="function") throw new TypeError(`whenInvalid, ${whenInvalid}, must be a function`);
if(rest.default!==undefined && typeof(rest.default)!=="symbol") throw new TypeError(`default, ${rest.default}, must be a symbol`);
return {
type: "symbol",
coerce,
required,
whenInvalid,
...rest,
validate: validateSymbol
}
}
const remoteProxy = ({json, variable,config, reactive, component}) => {
const type = typeof (config);
return new Proxy(json, {
get(target,property) {
if(property==="__remoteProxytarget__") return json;
return target[property];
},
async set(target, property, value) {
if(value && typeof(value)==="object" && value instanceof Promise) value = await value;
const oldValue = target[property];
if (oldValue !== value) {
let remotevalue;
if (type === "string") {
const href = new URL(config,window.location.href).href;
remotevalue = patch({target,property,value,oldValue},href,variable);
} else if(config && type==="object") {
let href;
if(config.path) href = new URL(config.path,window.location.href).href;
if(!config.patch) {
if(!href) throw new Error(`A remote path is required is no put function is provided for remote data`)
config.patch = patch;
}
remotevalue = config.patch({target,property,value,oldValue},href,variable);
}
if(remotevalue) {
await remotevalue.then((newjson) => {
if (newjson && typeof (newjson) === "object" && reactive) {
const target = variable.value?.__reactorProxyTarget__ ? json : variable.value;
Object.entries(newjson).forEach(([key,newValue]) => {
if(target[key]!==newValue) target[key] = newValue;
})
Object.keys(target).forEach((key) => {
if(!(key in newjson)) delete target[key];
});
if(variable.value?.__reactorProxyTarget__) {
const dependents = variable.value.__dependents__,
observers = dependents[property] || [];
[...observers].forEach((f) => {
if (f.cancelled) dependents[property].delete(f);
else f();
})
}
} else {
component.setVariableValue(variable.name,newjson)
//variable.value = json;
}
})
}
}
return true;
}
})
}
const patch = ({target,property,value,oldValue},href,variable) => {
return fetch(href, {
method: "PATCH",
body: JSON.stringify({property,value,oldValue}),
headers: {
"Content-Type": "application/json"
}
}).then((response) => {
if (response.status < 400) return response.json();
})
}
const get = (href,variable) => {
return fetch(href)
.then((response) => {
if (response.status < 400) return response.json();
})
}
const put = (href,variable) => {
return fetch(href, {
method: "PUT",
body: JSON.stringify(variable.value),
headers: {
"Content-Type": "application/json"
}
}).then((response) => {
if (response.status === 200) return response.json();
})
}
const handleRemote = async ({variable, functionalType, config=functionalType, component},doput) => {
let value;
if(!config.path) config.path = `./${variable.name}`;
if(config.path.endsWith("/")) config.path = `${config.path}${variable.name}`;
const href = new URL(config.path,new URL(config.base.replace("blob:","")).href).href;
if(!config.get || !config.put) {
if(!href) throw new Error(`A remote path is required if no put function is provided for remote data`)
if(!config.get) config.get = get;
if(!config.put && variable.reactive) config.put = put;
}
let areequal;
value = (doput
? (areequal = deepEqual(variable.value,config.previousValue,deepEqual.COMMUTATIVE)) ? variable.value : config.put(href,variable)
: config.get(href,variable));
if(config.ttl && !doput && !config.intervalId) {
config.intervalId = setInterval(async () => {
await handleRemote({variable, config, component});
//schedule();
},config.ttl);
}
if(variable.value===undefined) variable.value = value;
if(value && !areequal) {
const json = config.previousValue = await value;
if (json && typeof (json) === "object" && variable.reactive) {
component.setVariableValue(variable.name,remoteProxy({json, variable,config, component}));
// variable.value = remoteProxy({json, variable,config, component});
} else {
component.setVariableValue(variable.name,json);
}
}
}
const remote = (config,base=document.baseURI||window.location.href) => {
if(typeof(config)==="string") config = {path:config};
config.base = base;
return {
config:{...config},
handleRemote
}
}
const shared = () => {
return {
init({variable, component}) {
const name = variable.name,
set = variable.set || (() => {});
variable.shared = true;
variable.set = function(newValue) {
set(newValue);
if(this.shared) { // still shared
component.siblings.forEach((instance) => {
const svariable = instance.vars[name];
if(svariable?.shared) {
instance.setVariableValue(name, newValue);
}
});
}
}
// initialize
component.siblings.forEach((instance) => {
if(instance===component) return;
const svariable = instance.vars[name];
if(svariable?.shared) {
if(variable.value==null) {
if(svariable.value!=null) variable.value = instance.vars[name].value
} else instance.setVariableValue(name, variable.value);
}
});
}
}
}
const observed = () => {
return {
init({variable, component}) {
if(variable.observed) return;
const name = variable.name;
variable.value = component.hasAttribute(name) ? coerce(component.getAttribute(name), variable.type) : variable.value;
variable.observed = true;
component.observedAttributes.add(name);
}
}
}
const remoteGenerator = handleRemote;
export {ValidityState,any,array,boolean,duration,number,object,remote,shared,observed,remoteGenerator,string,symbol,reviver,html,css,script}