@stilljs/core
Version:
<div style="display:flex; justify-content: center"> <img src="@still/img/logo-no-bg.png" style="width: 5em;"/> </div>
1,248 lines (1,017 loc) • 66.6 kB
JavaScript
import { StillAppSetup } from "../../config/app-setup.js";
import { stillRoutesMap as DefaultstillRoutesMap } from "../../config/route.map.js";
import { $still, ComponentNotFoundException, ComponentRegistror } from "../component/manager/registror.js";
import { BaseComponent } from "../component/super/BaseComponent.js";
import { BehaviorComponent } from "../component/super/BehaviorComponent.js";
import { ViewComponent as DefaultViewComponent } from "../component/super/ViewComponent.js";
import { Router as DefaultRouter } from "../routing/router.js";
import { UUIDUtil } from "../util/UUIDUtil.js";
import { getRouter, getRoutesFile, getViewComponent } from "../util/route.js";
import { $stillconst, authErrorMessage } from "./constants.js";
import { StillError } from "./error.js";
const stillRoutesMap = await getRoutesFile(DefaultstillRoutesMap);
const Router = getRouter(DefaultRouter);
const ViewComponent = getViewComponent(DefaultViewComponent);
const $stillLoadScript = (path, className, base = null) => {
const prevScript = document.getElementById(`${path}/${className}.js`);
if (prevScript) return false;
const script = document.createElement('script');
script.src = `${base ? base : ''}${path}/${className}.js`;
script.id = `${path}/${className}.js`;
return script;
}
const ProduceComponentType = {
parentCmp: '',
registerCls: false,
urlRequest: false,
lone: false,
loneCntrId: null
};
class ProducedCmpResultType {
/** @type { DefaultViewComponent } */ newInstance;
/** @type { string } */ template;
/** @type { class | any } */ _class;
}
/** @template T */
export class SpotValue{
/**@param { T } type */
constructor(type, props = {}){ this.type = type, this.props = props }
}
export class Components {
/** @returns {{template, }} */
init() { }
static inject(cls) { return cls; }
template;
entryComponentPath;
entryComponentName;
/** @type { ViewComponent } */
component;
componentName;
static instance = null;
notAssignedValue = [undefined, null, ''];
stillCmpConst = $stillconst.STILL_COMPONENT;
stillAppConst = $stillconst.APP_PLACEHOLDER;
static componentPartsMap = {};
static annotations = {};
servicePath;
services = new Map();
static subscriptions = {};
static stAppInitStatus = true;
static baseUrl = Router.baseUrl;
static vendorPath = `${Components.obj().parseBaseUrl(Router.baseUrl)}@still/vendors`;
static parsingTracking = {};
static prevLoadingTracking = new Set();
/** @type { Array<String> } */
#cmpPermWhiteList = [];
void() { }
getTopLevelCmpId() {
const { TOP_LEVEL_CMP, ANY_COMPONT_LOADED } = $stillconst;
return `${TOP_LEVEL_CMP}`;
}
renderOnViewFor(placeHolder, cmp = null) {
const isLoneCmp = Router.clickEvetCntrId;
if (this.template instanceof Array) this.template = this.template.join('');
let cntr = document.getElementById(placeHolder);
if (isLoneCmp && isLoneCmp != 'null') cntr = document.getElementById(isLoneCmp);
else {
if (document.getElementById($stillconst.APP_PLACEHOLDER))
cntr = document.getElementById($stillconst.APP_PLACEHOLDER);
}
cntr.innerHTML = this.template;
if (isLoneCmp && cmp) setTimeout(() => {
Components.emitAction(cmp.getName());
}, 200);
Components.handleMarkedToRemoveParts();
}
/** @returns { ProducedCmpResultType } */
static async produceComponent(params = ProduceComponentType) {
if (!params.cmp) return;
const { cmp, parentCmp, registerCls, urlRequest: url } = params;
let clsName = cmp, isVendorCmp = (cmp || []).at(0) == '@', cmpPath,
template, folderPah, exception, baseUrl;
/** the bellow line clears previous component from memory
* @type { ViewComponent } */
let newInstance;
if (isVendorCmp) {
const clsPath = cmp.split('/');
clsName = clsPath.at(-1);
clsPath.pop();
const parsedUrl = Components.obj().parseBaseUrl(Components.baseUrl);
folderPah = `${parsedUrl}@still/vendors/${clsPath.join('/').slice(1)}`;
cmpPath = `${folderPah}/${clsName}`;
} else {
let cmpRoute = Router.routeMap[clsName]?.path;
if (!cmpRoute) {
StillError.handleStComponentNotFound('TypeError', parentCmp, clsName);
return;
}
if (cmpRoute.at(-1) == '/') cmpRoute = cmpRoute.slice(0, -1);
baseUrl = Router.getCleanUrl(url, clsName);
folderPah = `${baseUrl}${cmpRoute}`;
cmpPath = `${folderPah}/${clsName}`;
}
try {
const cmpCls = await import(`${cmpPath}.js`);
const parent = parentCmp ? { parent: parentCmp } : '';
newInstance = new cmpCls[clsName](parent);
Components.prevLoadingTracking.add(clsName);
newInstance.lone = !!params.loneCntrId || params.lone;
newInstance.loneCntrId = params.loneCntrId || Router.clickEvetCntrId;
newInstance.$parent = parentCmp;
if (!newInstance.template) {
let tmplFileUrl = cmpPath + '.html';
const { templateUrl } = newInstance;
if (templateUrl) tmplFileUrl = `${folderPah}/${templateUrl}`;
const result = await fetch(tmplFileUrl);
if (result.status == 404) {
StillError.handleInvalidTmplUrl('TypeError', newInstance, templateUrl);
exception = true;
return newInstance;
} else template = await result.text();
if (template) newInstance.template = template;
}
return {
newInstance, template: newInstance.template,
_class: !registerCls && !url ? null : cmpCls[clsName]
};
} catch (error) {
if (!exception)
StillError.handleStComponentNotFound(error, parentCmp, clsName);
return false;
}
}
static parseTemlpateCssToScope(template) {
const cssRE = /<style>[\s\S]*?<\/style>/gi;
let styles = '';
const scope = `scope_${UUIDUtil.newId()}`;
template = template.toString().replace(cssRE, (mt) => {
styles = styles + '\n' + mt
.replace('<style>', '<style> @scope(.' + scope + ') {\n')
.replace('</style>', '} </style>');
return '';
}, 'gi');
const styleRE = /[a-z0-9 \. \$\#\&\*\-\_\~\>\<\:\;\,\.\|\=\'\%\@\!\(\)\[\]\{\} ]*?{/ig
styles = styles.toString().replace(styleRE, (mtch) => mtch.trim());
return `<output class="${scope}" style="display: contents;">${template}<output>\n${styles}`;
}
static async processInitProperties() {
if(Object.keys(StillAppSetup.config.props).length){
//Disable the logs
const { default: df, error, warning, time } = StillAppSetup.config.props.logs;
if (error === false) window.console.error = () => '';
if (warning === false) window.console.warn = () => '';
if (df === false) {
window.console.log = () => '';
}
if (time !== true) {
window.console.time = () => '';
window.console.timeEnd = () => '';
}
}
}
async loadComponent() {
(async () => {
StillAppSetup.config.props = await Components.#loadConfig(StillAppSetup.configFile);
Object.freeze(StillAppSetup.config.props);
$still.context.currentView = await StillAppSetup.instance.init();
await Components.processInitProperties();
/** @type { ViewComponent } */
const currentView = $still.context.currentView;
if (currentView.template.indexOf(this.stillCmpConst) >= 0) {
$still.context.currentView = await (
await Components.produceComponent({ cmp: this.entryComponentName })
).newInstance;
if (
(!AppTemplate.get().isAuthN()
&& !$still.context.currentView.isPublic)
|| !Components.obj().isInWhiteList($still.context.currentView)
)
return document.write(authErrorMessage());
setTimeout(() => $still.context.currentView.parseOnChange(), 500);
StillAppSetup.register($still.context.currentView.constructor);
this.template = this.getHomeCmpTemplate($still.context.currentView);
ComponentRegistror.add($still.context.currentView.cmpInternalId, $still.context.currentView);
const { TOP_LEVEL_CMP, ST_HOME_CMP } = $stillconst;
this.template = currentView.template.replace(
this.stillCmpConst,
`<div id="${this.stillAppConst}" class="${TOP_LEVEL_CMP} ${ST_HOME_CMP}">
${this.template}
</div>`
);
this.template = (new BaseComponent).parseStSideComponent(
this.template, 'fixed-part', $stillconst.TOP_LEVEL_CMP
);
/** Very edge case */
const isHome = (new StillAppSetup).entryComponentName == this.entryComponentName;
if (isHome) {
this.template = (new BaseComponent).parseStSideComponent(
this.template, $still.context.currentView.cmpInternalId, $still.context.currentView.cmpInternalId
);
}
if (!$still.context.currentView.lone && !Router.clickEvetCntrId) {
$still.context.currentView.onRender();
this.renderOnViewFor('stillUiPlaceholder', $still.context.currentView);
}
setTimeout(() => Components.handleInPlacePartsInit($still.context.currentView, 'fixed-part'));
setTimeout(async () => {
Components.runAfterInit($still.context.currentView);
if (!Router.clickEvetCntrId) AppTemplate.injectToastContent();
});
StillAppSetup.get()['entryComponentId'] = $still.context.currentView.cmpInternalId;
return;
}
if (document.getElementById(this.stillAppConst))
this.renderOnViewFor(this.stillAppConst, $still.context.currentView);
else new Components().renderPublicComponent($still.context.currentView);
Components.runAfterInit($still.context.currentView)
})();
}
isAppLoaded = () => document.getElementById(this.stillAppConst);
/** @param { ViewComponent } cmp */
renderPublicComponent(cmp) {
if (cmp.isPublic || Components.obj().isInWhiteList(cmp)) {
Components.registerPublicCmp(cmp);
setTimeout(async () => await cmp.onRender());
this.template = `
<output class="${$stillconst.ANY_COMPONT_LOADED}" style="display:contents;">
${cmp.getBoundTemplate()}
</output>
`;
setTimeout(() => cmp.parseOnChange(), 500);
setTimeout(() => {
cmp.setAndGetsParsed = true;
(new Components).parseGetsAndSets(cmp)
}, 10);
this.renderOnViewFor('stillUiPlaceholder', cmp);
ComponentRegistror.add(cmp.cmpInternalId, cmp);
const cmpParts = Components.componentPartsMap[cmp.cmpInternalId];
setTimeout(() =>
Components.handleInPartsImpl(cmp, cmp.cmpInternalId, cmpParts)
);
setTimeout(() => Components.runAfterInit(cmp), 120);
Components.handleMarkedToRemoveParts();
Components.removeOldParts();
} else document.body.innerHTML = (authErrorMessage());
}
static registerPublicCmp = (cmp) =>
window['Public_' + cmp.constructor.name] = cmp;
/** @param {ViewComponent} cmp */
getHomeCmpTemplate(cmp) {
cmp.setUUID(cmp.getUUID());
cmp = (new Components()).getParsedComponent(cmp);
const loadCmpClass = $stillconst.ANY_COMPONT_LOADED;
const template = cmp.getBoundTemplate();
Components.registerPublicCmp(cmp);
const { TOP_LEVEL_CMP, ST_HOME_CMP1 } = $stillconst
return `<st-wrap
id="${TOP_LEVEL_CMP}"
class="${loadCmpClass} ${TOP_LEVEL_CMP} ${ST_HOME_CMP1}">
${template}
</st-wrap>`;
}
setComponentAndName(cmp, cmpName) {
this.component = cmp;
this.componentName = cmpName;
return this;
}
/** @param {ViewComponent} cmp */
defineSetter = (cmp, field) => {
if (cmp.myAnnotations()?.get(field)?.inject) return;
cmp.__defineGetter__(field, () => {
const optLst = cmp['stOptListFieldMap']?.get(field);
const r = {
value: cmp['$still_' + field], defined: true,
onChange: (callback = function () { }) => {
cmp[`$still${field}Subscribers`].push(callback);
},
firstPropag: false, onlyPropSignature: true,
};
if(optLst?.chkBox) r.isChkbox = true;
if(optLst?.radio) r.isRadio = true;
if(optLst?.multpl) {
if (r.value == '') r.value = [];
r.multpl = true;
}
const validator = BehaviorComponent.currentFormsValidators;
if (validator[cmp.cmpInternalId]) {
if (field in (validator[cmp?.constructor?.name] || []))
r['isValid'] = validator[cmp.constructor.name][field]['isValid'];
}
return r;
});
}
parseOnChange = () => this;
/** @param {ViewComponent} instance */
parseGetsAndSets(instance = null, allowfProp = false, field = null) {
const cmp = instance || this.component, o = this;
cmp.setAndGetsParsed = true;
const annot = cmp.runParseAnnot();
if (field) return parseField(field, cmp);
cmp.getProperties(allowfProp).forEach(field => parseField(field, cmp));
function parseField(field, cmp) {
const inspectField = cmp[field];
if(true === inspectField?.injectable || true === inspectField?.stSTI) return;
if (inspectField?.onlyPropSignature || inspectField?.name == 'Prop'
|| cmp.myAnnotations()?.get(field)?.prop || annot?.get(field)?.prop
) {
if (inspectField?.sTForm) {
cmp[field].validate = function () {
const formRef = field;
return BehaviorComponent.validateForm(`${cmp.cmpInternalId}-${formRef}`, cmp, cmp[field]);
}
cmp[field].reset = () => {
const formRef = field;
BehaviorComponent.validateForm(`${cmp.cmpInternalId}-${formRef}`, cmp, cmp[field], true);
document.getElementById(`fId_${field}`).reset();
}
return;
}
let listenerFlag = inspectField?.listenerFlag, inVal = inspectField?.inVal;
cmp[field] = cmp[field]?.value || cmp[field];
if (typeof inspectField == 'boolean') {
listenerFlag = `_stFlag${field}_${cmp.constructor.name}_change`;
cmp[field] = { inVal: inspectField }, inVal = inspectField;
}
if (listenerFlag) {
if (!('st_flag_ini_val' in cmp)) cmp['st_flag_ini_val'] = {};
cmp.st_flag_ini_val[field] = inVal;
const getValue = annot?.size ? inVal : inspectField.inVal;
cmp.__defineGetter__(field, () => getValue);
cmp.__defineSetter__(field, (val) => {
// This is to address the initial assignment for child component having (showIf) since ths instantiation is automatically by handleInPartsImpl
if (typeof val === 'object') {
if ('v' in val) {
val = val.v; delete cmp.st_flag_ini_val[field];
}
}
/** This is addressing the edge case where the (renderIf) is parsed after this setter is defined */
if (field in cmp.st_flag_ini_val && !(val?.parsed)) {
val = !cmp.st_flag_ini_val[field];
delete cmp.st_flag_ini_val[field];
}
/** This is for handling (renderIf) */
const elmList = document.getElementsByClassName(listenerFlag);
for (const elm of elmList) {
if(elm.style.display === 'none') elm.style.display = '';
else if(elm.style.display === '') elm.style.display = 'none';
if (val) elm.classList.remove($stillconst.PART_HIDE_CSS);
else elm.classList.add($stillconst.PART_HIDE_CSS);
}
cmp.__defineGetter__(field, () => val);
});
}
} else {
let value = cmp[field];
if (!cmp[field] && cmp[field] != 0) value = '';
Object.assign(cmp, { ['$still_' + field]: value });
Object.assign(cmp, { [`$still${field}Subscribers`]: [] });
o.defineSetter(cmp, field);
cmp.__defineSetter__(field, (newValue) => {
if(cmp['stOptListFieldMap']?.get(field)?.multpl)
cmp['$still_' + field] = newValue;
cmp.__defineGetter__(field, () => newValue);
//Address multi selection combobox: TOUCHING HERE
cmp['$still_' + field] = newValue;
o.defineSetter(cmp, field);
setTimeout(async () => await cmp.stOnUpdate());
if (cmp[`$still${field}Subscribers`].length > 0) {
setTimeout(() => cmp[`$still${field}Subscribers`].forEach(
subscriber => subscriber(cmp['$still_' + field])
));
}
if (cmp.$stillClassLvlSubscribers.length > 0)
setTimeout(() => cmp.notifySubscribers(cmp.getStateValues()));
if (cmp[field]?.defined || cmp.dynLoopObject) {
Components.firstPropagation[`${cmp.cmpInternalId}-${field}`] = true;
o.propageteChanges(cmp, field, { A: !cmp['st'+field+'cbRem'], C: cmp['st'+field+'cbClk'] });
}
delete cmp['st'+field+'cbRem'];
delete cmp['st'+field+'cbClk'];
});
const firstPropagateTimer = setInterval(() => {
//Work in the garbage collector for this Components.firstPropagation flag
if(Components?.firstPropagation[`${cmp.cmpInternalId}-${field}`]) {
clearInterval(firstPropagateTimer);
o.propageteChanges(cmp, field);
}
else if ('value' in cmp[field] && !Components?.firstPropagation[`${cmp.cmpInternalId}-${field}`]) {
clearInterval(firstPropagateTimer);
if (!o.notAssignedValue.includes(cmp['$still_' + field])) o.propageteChanges(cmp, field);
cmp[field].firstPropag = true;
}
}, 200);
}
}
return this;
}
static firstPropagation = {};
/** @param { ViewComponent } cmp */
propageteChanges(cmp, field, chkBoxOpts = {}) {
const cpName = cmp.cmpInternalId.replace('/', '').replace('@', ''), f = field;
const cssRef = `.listenChangeOn-${cpName}-${f}`;
const subscribers = document.querySelectorAll(cssRef);
const cssRefCombo = `.listenChangeOn-${cpName}-${f}-combobox`;
const subscribersCombo = document.querySelectorAll(cssRefCombo);
const stateChange = `.state-change-${cpName}-${f}`;
const stateChangeSubsribers = document.querySelectorAll(stateChange);
const { isChkbox, isRadio, multpl, value } = cmp[f];
if(isChkbox || (multpl && !cmp['stClk' + f])){
// chkBoxOpts.A = add, chkBoxOpts.C = click
if((chkBoxOpts.A && !chkBoxOpts.C) || multpl){
const action = multpl ? 'selected' : 'checked';
let waitSec = multpl ? new Number($still.multplWaitSec) : 0; //Adding a delay to allow th combobox to render
value.forEach(v => {
setTimeout(() => {
const opt = document.querySelector(`.${cpName}-${f}-val-${v}`.replace(/\s/g,'-'));
if(opt) {
opt[action] = true;
const empty = document.querySelector(`.${cpName}-${f}empty`);
if(empty) empty.selected = false;
}
},waitSec);
});
if($still.multplWaitSec > 0) $still.multplWaitSec = 0;
}
}
if(isRadio){
if(chkBoxOpts.A && !chkBoxOpts.C){
const opt = document.querySelector(`.${cpName}-${f}-val-${value}`);
if(opt) {
//cmp['$still_' + f] = value;
opt.checked = true;
}
}
}
if (stateChangeSubsribers)
stateChangeSubsribers.forEach(s => s.innerHTML = cmp['$still_' + f]);
if (subscribers && !cmp['stOptListFieldMap']?.has(f))
subscribers.forEach(/**@type {HTMLElement}*/elm => this.dispatchPropagation(elm, f, cmp));
if (subscribersCombo) {
subscribersCombo.forEach(/**@type {HTMLElement}*/elm => {
setTimeout(() => this.propageteToInput(elm, f, cmp));
});
}
}
/**
* @param { HTMLElement } elm
* @param { ViewComponent } cmp
*/
dispatchPropagation(elm, field, cmp) {
if (elm.tagName == 'SELECT')
return this.propageteToSelect(elm, field, cmp);
if (elm.tagName == 'INPUT')
return this.propageteToInput(elm, field, cmp);
return this.propagetToHTMLContainer(elm, field, cmp);
}
/**
* @param { HTMLElement } elm
* @param { ViewComponent } cmp
*/
propageteToInput(elm, field, cmp) {
elm.value = cmp['$still_' + field];
}
/**
* @param { HTMLElement } elm
* @param { ViewComponent } cmp
*/
propageteToSelect(elm, field, cmp) {
(async () => {
const container = document.createElement('optgroup');
const old = elm?.getElementsByTagName('optgroup')[0];
await this.parseAndAssigneValue(elm, field, cmp, container);
if (old) elm.removeChild(old);
elm.insertAdjacentElement('beforeend', container);
})();
}
/**
* @param { HTMLElement } elm
* @param { ViewComponent } cmp
*/
propagetToHTMLContainer(elm, field, cmp) {
(async () => {
let container = document.createElement(elm.tagName);
let prevClass = elm.getAttribute('newCls');
prevClass = elm.getAttribute('class').replace(prevClass, '');
container.className = `${prevClass}`;
container = await this.parseAndAssigneValue(elm, field, cmp, container);
if (elm.tagName == 'TBODY')
elm.parentNode.insertAdjacentElement('beforeend', container);
else {
const loopCntr = elm.querySelector(`.loop-container-${cmp.cmpInternalId}`);
if (loopCntr) elm.removeChild(loopCntr);
const finalCtnr = document.createElement('output');
finalCtnr.innerHTML = container.innerHTML;
finalCtnr.className = `loop-container-${cmp.cmpInternalId}`;
finalCtnr.style.display = 'contents';
elm.insertAdjacentElement('beforeend', finalCtnr);
}
})();
}
/**
* @param { HTMLElement } elm
* @param { ViewComponent } cmp
*/
async parseAndAssigneValue(elm, field, cmp, container) {
const hash = elm.getAttribute('hash'), cmpId = cmp.cmpInternalId;
container.classList.add($stillconst.SUBSCRIBE_LOADED);
let tmpltContent, childCmp, f = elm.dataset.field;
if (elm.tagName == 'SELECT') {
const childs = elm.querySelectorAll('option');
const placeholder = elm.getAttribute('placeholder') || 'Select an option';
if (childs[0].outerHTML.toString().indexOf('{item.') > 0){
tmpltContent = childs[0].outerHTML.toString();
const valField = tmpltContent.split('value')[1].split('"')[1];
const cls = `${cmpId}-${f}-val-${valField}st`;
tmpltContent = tmpltContent.replace('<option','<option class="'+cls+'"');
}
let result = `<option value='' class="${cmpId}-${f}empty" selected>${placeholder}</option>`;
container.innerHTML = await this.parseForEachTemplate(tmpltContent, cmp, field, result);
return container;
} else {
tmpltContent = elm.firstElementChild.outerHTML.toString();
const stDSource = elm.firstElementChild.getAttribute('loopDSource');
childCmp = {
stElement: elm.firstElementChild.getAttribute('componentRef'),
props: JSON.parse(elm.firstElementChild.getAttribute('props')),
stDSource: ![false, 'false'].includes(stDSource)
}
}
/** Get the previous table body */
const oldContainer = elm.parentNode.querySelector(`.${hash}`);
/** Check if it exists previous table body and remove it */
if (oldContainer){
try {
elm?.parentNode?.removeChild(oldContainer);
} catch (error) {}
}
container.classList.add(hash);
container.innerHTML = await this.parseForEachTemplate(
tmpltContent, cmp, field, '', childCmp
);
return container;
}
async parseForEachTemplate(tmpltContent, cmp, field, result, childCmp = null) {
let template = tmpltContent.replace('display:none;', ''), childTmpl, childResult = '';
if(template.indexOf('</st-lstgp>')) template = template.replace('display:none;', '');
if (cmp['$still_' + field] instanceof Array) {
if (childCmp?.stElement) {
const childInstance = await (await Components.produceComponent({
cmp: childCmp.stElement,
parentCmp: cmp, registerCls: true
}));
childTmpl = childInstance.template;
let fields = Object.entries(childCmp.props), noFieldsMap;
if (fields.length == 0) noFieldsMap = true;
for (const rec of cmp['$still_' + field]) {
/** @type { ViewComponent } */
const inCmp = new childInstance._class();
inCmp.cmpInternalId = 'dynamic-' + inCmp.getUUID();
inCmp.template = childTmpl;
inCmp.dynLoopObject = true;
inCmp.stillElement = true;
inCmp.parentVersionId = cmp.versionId;
if (noFieldsMap && childCmp.stDSource)
fields = Object.entries(cmp['$still_' + field][0]);
await inCmp.onRender();
childResult += await this
.replaceBoundFieldStElement(inCmp, fields, rec, noFieldsMap)
.getBoundTemplate();
setTimeout(() => inCmp.parseOnChange(), 500);
ComponentRegistror.add(inCmp.cmpInternalId, inCmp);
setTimeout(() => {
(new Components)
.parseGetsAndSets(
ComponentRegistror.component(inCmp.cmpInternalId)
)
}, 10);
};
} else {
cmp['$still_' + field].forEach((rec) => {
let parsingTemplate = template;
let fields = Object.entries(rec);
result += this.replaceBoundField(parsingTemplate, fields);
});
}
}
return childCmp?.stElement ? childResult : result;
}
replaceBoundField(parsingTemplate, fields) {
for (const [f, v] of fields) {
parsingTemplate = parsingTemplate.replaceAll(`{item.${f}}`, (m, pos, tmpl) => {
const isCls = tmpl.slice(pos + m.length).startsWith('st');
return isCls ? new String(v)?.replace(/\s/g,'-')+' ' : v;
});
}
return parsingTemplate.replaceAll('($event', '(event');
}
/**
* @param { ViewComponent } obj
* @returns { ViewComponent }
* */
replaceBoundFieldStElement(obj, fields, rec, noFieldsMap) {
if (noFieldsMap) {
for (const [f, v] of Object.entries(rec)) {
if (f in obj) {
if (typeof v == 'string') obj[f] = v?.replace('item.', '')?.trim();
else obj[f] = v;
}
}
} else {
for (const [f, v] of fields) {
if (typeof v == 'string') obj[f] = rec[v?.replace('item.', '')?.trim()];
else obj[f] = rec[v];
}
}
return obj;
}
/** @param {ViewComponent} cmp */
defineNewInstanceMethod() { return this; }
markParsed() {
Object.assign(this.component, { stillParsedState: true });
return this;
}
/** @param {ViewComponent} cmp */
getParsedComponent(cmp) {
const componentName = cmp.getName();
window[componentName] = cmp;
const parsing = this
.setComponentAndName(window[componentName], cmp.getName())
.defineNewInstanceMethod();
if (!Components.parsingTracking[cmp.cmpInternalId]) {
Components.parsingTracking[cmp.cmpInternalId] = true;
cmp.setAndGetsParsed = true;
setTimeout(() => {
parsing.parseGetsAndSets().markParsed()
});
} else {
delete Components.parsingTracking[cmp.cmpInternalId];
}
return window[componentName];
}
/**
* @param {ViewComponent} cmp
* @returns {ViewComponent}
*/
getNewParsedComponent(cmp, cmpName = null, allowProps = false) {
this
.setComponentAndName(cmp, cmpName || cmp.getName())
.defineNewInstanceMethod()
.parseOnChange();
if (!Components.parsingTracking[cmp.cmpInternalId]) {
Components.parsingTracking[cmp.cmpInternalId] = true;
cmp.setAndGetsParsed = true;
this.parseGetsAndSets(null, allowProps);
} else
delete Components.parsingTracking[cmp.cmpInternalId];
this.markParsed();
return cmp;
}
static unloadLoadedComponent(cntrPlaceholder = null) {
return new Promise((resolve) => {
if (cntrPlaceholder) cntrPlaceholder.innerHTML = '';
const { ANY_COMPONT_LOADED } = $stillconst;
/** @type { Array<HTMLElement|ViewComponent> } */
const cmps = document.querySelectorAll(`.${ANY_COMPONT_LOADED}`);
for (const cmp of cmps) cmp.style.display = 'none';
resolve([]);
})
}
/** @param {ViewComponent} cmp */
static async reloadedComponent(cmp, isHome) {
const isUnAuthn = !AppTemplate.get().isAuthN();
let cmpName = cmp.constructor.name, template;
if ((!cmp.isPublic && isUnAuthn) && !Components.obj().isInWhiteList(cmp)) {
if (document.querySelector(`.${$stillconst.ST_FIXE_CLS}`)) {
return document.getElementById($stillconst.UI_PLACEHOLDER)
.insertAdjacentHTML('afterbegin', authErrorMessage());
} else return document.write(authErrorMessage());
}
/** @type { ViewComponent } */
let newInstance = await (
await Components.produceComponent({ cmp: cmpName, loneCntrId: cmp.loneCntrId })
).newInstance;
newInstance.setUUID(cmp.getUUID());
template = newInstance.getBoundTemplate(null, true);
newInstance = (new Components()).getParsedComponent(newInstance);
newInstance.setRoutableCmp(true);
let elmRef = cmp.getUUID();
if (isHome) {
elmRef = isUnAuthn ? $stillconst.ST_HOME_CMP : $stillconst.ST_HOME_CMP1;
}
let container = cmp.loneCntrId && cmp.loneCntrId?.trim() != 'null'
? document.getElementById(cmp.loneCntrId)
: document.querySelector(`.${elmRef}`);
const previousContainer = container;
if (!previousContainer) {
await newInstance.onRender();
container = Components.getCmpViewContainer(cmpName, newInstance.cmpInternalId, newInstance.getTemplate());
ComponentRegistror.add(newInstance.cmpInternalId, newInstance);
} else {
await newInstance.onRender();
container.innerHTML = newInstance.getTemplate();
}
if (newInstance?.lone) setTimeout(() => {
Components.emitAction(newInstance.getName(), newInstance.cmpInternalId);
}, 200);
if (!previousContainer) {
const cmpParts = Components.componentPartsMap[newInstance.cmpInternalId];
Components.handleInPartsImpl(newInstance, newInstance.cmpInternalId, cmpParts);
}
container.style.display = 'contents';
const isTHereFixedPart = document.querySelector(`.${$stillconst.ST_FIXE_CLS}`);
if (!isTHereFixedPart && isUnAuthn || (isTHereFixedPart && isUnAuthn)
) {
Router.callCmpAfterInit(
null, isHome, isHome ? Router.appPlaceholder : null
);
}
Components.runAfterInit(newInstance)
setTimeout(async () => newInstance.parseOnChange(), 200);
if (!newInstance.setAndGetsParsed) {
newInstance.setAndGetsParsed = true;
setTimeout(() => {
(new Components).parseGetsAndSets(
ComponentRegistror.component(newInstance.cmpInternalId)
)
}, 10);
}
if (cmp.loneCntrId)
ComponentRegistror.add(newInstance.cmpInternalId, newInstance);
if (cmp.isPublic) this.registerPublicCmp(newInstance);
else ComponentRegistror.add(cmpName, newInstance);
}
static getCmpViewContainer(cmpName, cmpId, template) {
let cntr = document.querySelector(`.cmp-name-page-view-${cmpName}`);
if (!cntr) {
cntr = document.createElement('output');
cntr.style.display = 'contents';
cntr.id = `${cmpId}-check`;
cntr.className = `cmp-name-page-view-${cmpName}`;
cntr.innerHTML = template;
let appContr = document.getElementById($stillconst.APP_PLACEHOLDER);
if (!appContr) appContr = document.getElementById($stillconst.UI_PLACEHOLDER);
appContr.insertAdjacentHTML('afterbegin', cntr.outerHTML);
}
return cntr;
}
static unloadApp() {
const appContainer = document.getElementById($stillconst.APP_PLACEHOLDER);
const parent = appContainer.parentElement;
parent.removeChild(appContainer);
}
/** @param { ViewComponent } parentCmp */
static handleInPlaceParts(parentCmp, cmpInternalId = null) {
/** @type { Array<ComponentPart> } */
let cmpParts = Components.componentPartsMap[cmpInternalId || parentCmp.cmpInternalId];
if (cmpInternalId == true)
cmpParts = Object.values(Components.componentPartsMap)[1];
Components.handleInPartsImpl(parentCmp, cmpInternalId, cmpParts);
}
/** @param { ViewComponent } parentCmp */
static handleInPlacePartsInit(parentCmp, cmpInternalId = null) {
const allParts = Object.entries(Components.componentPartsMap);
for (const [parentId, cmpParts] of allParts) {
const parentCmp = $still.context.componentRegistror.componentList[parentId]
if (parentCmp?.instance?.lone) {
Components.subscribeAction(
parentCmp.instance.getName(),
(placeHolderId) => {
Components.handleInPartsImpl(
parentCmp?.instance, parentId, cmpParts, placeHolderId
);
}
);
} else
Components.handleInPartsImpl(parentCmp?.instance, parentId, cmpParts);
}
}
static getPartPlaceHolder(parentCmp, cmpInternalId, placeHolderRef) {
if (placeHolderRef)
return document.getElementsByClassName(`still-placeholder${placeHolderRef}`);
return cmpInternalId == 'fixed-part'
? document.getElementById(`stillUiPlaceholder`).getElementsByTagName('still-placeholder')
: document.getElementsByClassName(`still-placeholder${parentCmp?.getUUID()}`)
}
/**
* @param {BaseComponent} parentCmp
* @param {*} cmpInternalId
* @param {*} cmpParts
*/
static handleInPartsImpl(parentCmp, cmpInternalId, cmpParts, placeHolderRef = null) {
if ((!cmpParts || !parentCmp) && cmpInternalId != 'fixed-part') return;
const placeHolders = Components
.getPartPlaceHolder(parentCmp, cmpInternalId, placeHolderRef);
/** Get all <st-element> component to replace with the actual component template */
if (cmpInternalId != 'fixed-part')
if (parentCmp) parentCmp.versionId = UUIDUtil.newId();
const cmpVersionId = cmpInternalId == 'fixed-part' ? null : parentCmp.versionId;
for (let idx = 0; idx < cmpParts.length; idx++) {
const parentClss = placeHolders[idx]?.parentNode?.classList;
/** Preventing this component to be instantiated in case it should not be
* rendered due to the (renderIf) flag value is found to be false */
if (parentClss?.contains($stillconst.PART_REMOVE_CSS))
continue;
const { proxy, component, props, annotations, ref } = cmpParts[idx];
if (component == undefined) continue;
(async () => {
/** TODO: Dynamic import of assets of a vendor component */
/* if (isVendorCmp) {
const imports = await cmpCls[realClsName]?.importAssets();
imports?.scripts?.forEach(r => {
BaseComponent.importScript(`${cmpPath}/${r}`);
});
} */
const instance = await (
await Components.produceComponent({ cmp: component, parentCmp })
).newInstance;
let cmpName, canHandle = true;
if (!Components.obj().canHandleCmpPart(instance))
return;
instance.dynCmpGeneratedId = `st_${UUIDUtil.numberId()}`;
/** In case the parent component is Lone component, then child component will also be */
instance.lone = parentCmp?.lone;
await instance.onRender();
instance.cmpInternalId = `dynamic-${instance.getUUID()}${component}`;
instance.stillElement = true;
instance.proxyName = proxy;
instance.nstngCount = parentCmp?.nstngCount ? parentCmp.nstngCount + 1 : 1;
if(cmpInternalId == 'fixed-part') instance['isStFixed'] = true;
ComponentRegistror.add(instance.cmpInternalId, instance);
if (instance)
cmpName = 'constructor' in instance ? instance.constructor.name : null;
/** TOUCH TO REINSTANTIATE */
const cmp = (new Components).getNewParsedComponent(instance, cmpName, true);
cmp.parentVersionId = cmpVersionId;
//if (cmpInternalId != 'fixed-part') {
Components.parseProxy(proxy, cmp, parentCmp, annotations);
cmp['stName'] = cmpName;
StillAppSetup.register(cmp);
const allProps = Object.entries(props);
for (let [prop, value] of allProps) {
if (prop == 'ref') ComponentRegistror.add(value, cmp);
//Proxy gets ignored becuase it was assigned above and it should be the child class
if (prop != 'proxy' && prop != 'component') {
if (prop.charAt(0) == '(' && prop.at(-1) == ")") {
const method = prop.replace('(', '').replace(')', '');
cmp[method] = (...param) => parentCmp[value.split('(')[0]](...param);
continue;
}
let prefix = String(value).toLowerCase();
if (prop in instance && !value?.startsWith('parent.') && !value?.startsWith('self.')) {
//Because this assignement will trigger getters for flag, passing an object
//with v field will allow identify that this is framework initial instance assignement
const _value = ['false', false].includes(value) ? { v: false, stBVal: true } : ['true', true].includes(value) ? { v: true, stBVal: true } : value;
if(cmpInternalId != 'fixed-part') instance[prop] = _value;
if(cmpInternalId == 'fixed-part') {
instance[prop] = _value?.stBVal ? _value.v : _value;
instance.__defineGetter__(prop, () => _value?.stBVal ? _value.v : _value);
}
continue;
}
if (prefix.startsWith('parent.')) prefix = 'parent.';
else if (prefix.startsWith('self.')) prefix = 'self.';
else prefix = '';
if (prefix == '') {
value = value.trim();
if (!isNaN(value) || (value.startsWith('\'') && value.endsWith('\'')))
cmp[prop] = value;
}
else if (String(value).toLowerCase().startsWith(prefix) || prefix == '') {
const parentProp = parentCmp[value.replace(prefix, '').trim()];
if (parentProp?.onlyPropSignature) cmp[prop] = parentProp.value;
else cmp[prop] = parentProp?.value || parentProp;
} else
cmp[prop] = value;
}
/** Replace the parent component on the registror So that it get's
* updated with the new and fresh data, properties and proxies */
ComponentRegistror.add(
parentCmp?.constructor?.name,
parentCmp
);
}
//}
//replaces the actual template in the <st-element> component placeholder
placeHolders[idx]
?.insertAdjacentHTML('afterbegin', cmp.getBoundTemplate());
// Renders second level of nesting
if(cmp?.nstngCount == 1) Components.handleInPlaceParts(cmp);
setTimeout(async () => {
/** Runs the load method which is supposed to implement what should be run
* for the component to be displayed accordingly in the User interface */
await cmp.load();
setTimeout(() => Components.runAfterInit(cmp), 120);
if ((idx + 1) == cmpParts.length && cmpInternalId != 'fixed-part')
setTimeout(() => Components.emitAction('runImport'), 120);
Components.handleMarkedToRemoveParts();
});
})();
}
if (cmpInternalId != 'fixed-part') {
Components.subscribeAction('runImport', () => {
if ('importAssets' in parentCmp) {
const imports = parentCmp?.importAssets(), assets = [];
if (imports?.scripts) assets.push(...imports?.scripts)
if (imports?.styles) assets.push(...imports?.styles)
if ((assets || []).length)
assets.forEach(BaseComponent.importScript);
}
})
}
}
/** @param { ViewComponent } cmp */
canHandleCmpPart(cmp) {
if (Components.obj().isInWhiteList(cmp)) return true;
if (AppTemplate.get().isAuthN() && !Components.obj().isInWhiteList(cmp))
return false;
if ((!AppTemplate.get().isAuthN()
&& !cmp?.isPublic)
|| !Components.obj().isInWhiteList(cmp))
return false;
return true;
}
static handleMarkedToRemoveParts() {
setTimeout(() => {
const markedToRemoveElm = document
.getElementsByClassName($stillconst.PART_REMOVE_CSS);
for (const elm of markedToRemoveElm) elm.innerHTML = '';
}, 500);
}
static removeVersionId;
static setRemovingPartsVersionId(versionId) {
Components.removeVersionId = versionId;
}
static removeOldParts() {
(async () => {
const registror = $still.context.componentRegistror.componentList;
await registror[Router.preView?.cmpInternalId]?.instance?.stOnUnload();
await registror[Router.preView?.constructor?.name]?.instance?.stOnUnload();
delete registror[Router.preView?.cmpInternalId];
delete registror[Router.preView?.constructor?.name];
const versionId = Components.removeVersionId;
setTimeout(() => {
if (versionId) {
document
.querySelectorAll(`.loop-container-${Router.preView.cmpInternalId}`)
.forEach(elm => elm.innerHTML = '');
const list = Object
.entries(registror)
.filter(r => r[1].instance.parentVersionId == versionId)
.map(r => r[0]);
list.forEach(
async versionId => {
await registror[versionId]?.instance?.stOnUnload();
delete registror[versionId]
}
);
}
Object
.entries($still.context.componentRegistror.componentList)
.forEach(async r => {
if (r[1].instance.navigationId < Router.navCounter) {
await registror[r[0]]?.instance?.stOnUnload();
delete $still.context.componentRegistror.componentList[r[0]]
}
})
})
})()
}
static parseProxy(proxy, cmp, parentCmp, annotations) {
if (proxy) {
const sbscbrs = parentCmp[proxy].subscribers;
parentCmp[proxy] = cmp;
if (sbscbrs && sbscbrs.length) sbscbrs.forEach(async cb => await cb());
}
}
/** @type { { Object<[key]: ViewComponent> } } */
static afterIniSubscriptions = {};
/**
* @param {string} event
* @param {ViewComponent} cmp
*/
static subscribeStAfterInit = (cpmName, cmp) =>
Components.afterIniSubscriptions[cpmName] = cmp;
/** @param {string} event */
static emitAfterIni(cpmName) {
(async () => {
const registror = $still.context.componentRegistror.componentList;
Components.runAfterInit(registror[cpmName].instance);
})
delete Components.afterIniSubscriptions[cpmName];
}
/** @returns { boolean } */
static checkStInit(cpmName) {
return cpmName in Components.afterIniSubscriptions;
}
static subscribeAction(actonName, action) {
if (actonName in Components.subscriptions) {
if (Components.subscriptions[actonName].status == $stillconst.A_STATUS.DONE) {
action();
return;
}
}
if (!(actonName in Components.subscriptions)) {
const status = $stillconst.A_STATUS.PENDING
Components.subscriptions[actonName] = { status, actions: [], count: 0 };
}
const count = Components.subscriptions[act