UNPKG

@cimo/jsmvcfw

Version:

Javascript mvc framework. Light, fast and secure.

484 lines 19.7 kB
import { createVirtualNode, updateVirtualNode } from "./JsMvcFwDom.js"; import Emitter from "./JsMvcFwEmitter.js"; let urlRoot = ""; let appLabel = ""; const virtualNodeObject = {}; const renderTriggerObject = {}; const variableBindRegistry = {}; const variableLoadedList = {}; const variableEditedList = {}; const variableRenderUpdateObject = {}; const variableHookObject = {}; const variableLinkPendingList = []; const controllerList = []; let cacheVariableProxyWeakMap = new WeakMap(); const emitterObject = {}; let observerWeakMap = new WeakMap(); let callbackObserverWeakMap = new WeakMap(); const variableRenderUpdate = (controllerName) => { if (emitterObject[controllerName] && !variableRenderUpdateObject[controllerName]) { variableRenderUpdateObject[controllerName] = true; Promise.resolve().then(() => { const renderTrigger = renderTriggerObject[controllerName]; if (renderTrigger) { renderTrigger(); } emitterObject[controllerName].emit("variableChanged"); variableRenderUpdateObject[controllerName] = false; }); } }; const variableProxy = (stateLabel, stateValue, controllerName) => { if (typeof stateValue !== "object" || stateValue === null) { return stateValue; } const cache = cacheVariableProxyWeakMap.get(stateValue); if (cache) { return cache; } const proxy = new Proxy(stateValue, { get(target, property, receiver) { const result = Reflect.get(target, property, receiver); if (typeof result === "object" && result !== null) { return variableProxy(stateLabel, result, controllerName); } return result; }, set(target, property, newValue, receiver) { if (variableEditedList[controllerName] && !variableEditedList[controllerName].includes(stateLabel)) { variableEditedList[controllerName].push(stateLabel); } const isSuccess = Reflect.set(target, property, newValue, receiver); if (isSuccess) { variableRenderUpdate(controllerName); } return isSuccess; }, deleteProperty(target, property) { if (variableEditedList[controllerName] && !variableEditedList[controllerName].includes(stateLabel)) { variableEditedList[controllerName].push(stateLabel); } const isSuccess = Reflect.deleteProperty(target, property); if (isSuccess) { variableRenderUpdate(controllerName); } return isSuccess; } }); cacheVariableProxyWeakMap.set(stateValue, proxy); return proxy; }; const variableBindItem = (label, stateValue, controllerName) => { let _state = variableProxy(label, stateValue, controllerName); const _listenerList = []; return { get state() { return _state; }, set state(value) { if (variableEditedList[controllerName] && !variableEditedList[controllerName].includes(label)) { variableEditedList[controllerName].push(label); } _state = variableProxy(label, value, controllerName); for (const listener of _listenerList) { listener(_state); } variableRenderUpdate(controllerName); }, listener(callback) { _listenerList.push(callback); } }; }; const variableWatch = (controllerName, callback) => { if (!emitterObject[controllerName]) { emitterObject[controllerName] = new Emitter(); } const emitter = emitterObject[controllerName]; emitter.on("variableChanged", () => { const editedList = variableEditedList[controllerName] || []; callback((groupObject) => { for (const group of groupObject) { let isAllEdited = true; for (let b = 0; b < group.list.length; b++) { const key = group.list[b]; if (editedList.indexOf(key) === -1) { isAllEdited = false; break; } } if (isAllEdited) { group.action(); for (const key of group.list) { const index = editedList.indexOf(key); if (index !== -1) { editedList.splice(index, 1); } } } } variableEditedList[controllerName] = editedList; }); }); }; const elementHook = (elementContainer, controllerValue) => { const elementHookList = elementContainer.querySelectorAll("[jsmvcfw-elementHookName]"); const hookObject = {}; for (const elementHook of elementHookList) { const attribute = elementHook.getAttribute("jsmvcfw-elementHookName"); if (attribute) { const matchList = attribute.match(/^([a-zA-Z0-9]+)_\d+$/); const baseKey = matchList ? matchList[1] : attribute; if (hookObject[baseKey]) { if (Array.isArray(hookObject[baseKey])) { hookObject[baseKey].push(elementHook); } else { hookObject[baseKey] = [hookObject[baseKey], elementHook]; } } else { if (matchList) { hookObject[baseKey] = [elementHook]; } else { hookObject[attribute] = elementHook; } } } } const hookObjectMerged = { ...hookObject }; const hookObjectCurrent = controllerValue.hookObject || {}; for (const [key, value] of Object.entries(hookObjectCurrent)) { if (hookObjectMerged[key]) { continue; } const valueList = Array.isArray(value) ? value : [value]; const valueConnectedList = valueList.filter((item) => item && item.isConnected); if (!valueConnectedList.length) { continue; } hookObjectMerged[key] = valueConnectedList.length === 1 ? valueConnectedList[0] : valueConnectedList; } controllerValue.hookObject = hookObjectMerged; }; const variableLinkReference = (value) => { if (typeof value !== "object" || value === null) { return false; } const target = value; return target.__jsmvcfwType === "variableLink" && typeof target.controllerNameSource === "string"; }; const variableLinkClone = (value) => { if (value === null || typeof value !== "object") { return value; } if (Array.isArray(value)) { return [...value]; } return { ...value }; }; const variableLinkResolve = (target, label, targetBind) => { const sourceControllerObject = variableBindRegistry[target.controllerNameSource]; const sourceBind = sourceControllerObject ? sourceControllerObject[label] : null; if (!sourceBind) { return false; } let isSyncing = false; const syncSourceToTarget = (nextValue) => { if (isSyncing || Object.is(targetBind.state, nextValue)) { return; } isSyncing = true; targetBind.state = nextValue; isSyncing = false; }; const syncTargetToSource = (nextValue) => { if (isSyncing || Object.is(sourceBind.state, nextValue)) { return; } isSyncing = true; sourceBind.state = nextValue; isSyncing = false; }; syncSourceToTarget(sourceBind.state); sourceBind.listener(syncSourceToTarget); targetBind.listener(syncTargetToSource); return true; }; const variableLinkPendingFlush = () => { for (let a = variableLinkPendingList.length - 1; a >= 0; a--) { const pending = variableLinkPendingList[a]; const isResolved = variableLinkResolve(pending.target, pending.label, pending.targetBind); if (isResolved) { variableLinkPendingList.splice(a, 1); } } }; const normalizeVirtualNode = (node) => { if (Array.isArray(node)) { return { tag: "div", propertyObject: { style: "display: contents;" }, childrenList: node }; } return node; }; export const setUrlRoot = (urlRootValue) => (urlRoot = urlRootValue); export const getUrlRoot = () => urlRoot; export const setAppLabel = (appLabelValue) => (appLabel = appLabelValue); export const getAppLabel = () => appLabel; export const getControllerList = () => controllerList; export const renderTemplate = (controllerValue, controllerParent, callback) => { const controllerName = controllerValue.constructor.name; if (!controllerParent) { controllerList.push({ parent: controllerValue, childrenList: [] }); } else { for (const controller of controllerList) { if (controllerParent.constructor.name === controller.parent.constructor.name) { controller.childrenList.push(controllerValue); break; } } } controllerValue.variable(); const renderTrigger = () => { if (!controllerParent) { let virtualNodeNew = controllerValue.view(); virtualNodeNew = normalizeVirtualNode(virtualNodeNew); if (!virtualNodeNew || typeof virtualNodeNew !== "object" || !virtualNodeNew.tag) { throw new Error(`@cimo/jsmvcfw - JsMvcFw.ts - renderTrigger() => Invalid virtual node returned by controller "${controllerName}"!`); } const elementRoot = document.getElementById("jsmvcfw_app"); if (!elementRoot) { throw new Error("@cimo/jsmvcfw - JsMvcFw.ts - renderTrigger() => Root element #jsmvcfw_app not found!"); } const virtualNodeOld = virtualNodeObject[controllerName]; if (!virtualNodeOld) { const elementVirtualNode = createVirtualNode(virtualNodeNew); elementRoot.innerHTML = ""; elementRoot.appendChild(elementVirtualNode); if (callback) { callback(); } } else { const elementFirstChild = elementRoot.firstElementChild; if (elementFirstChild) { updateVirtualNode(elementFirstChild, virtualNodeOld, virtualNodeNew); } } virtualNodeObject[controllerName] = virtualNodeNew; elementHook(elementRoot, controllerValue); return; } const parentContainer = document.querySelector(`[jsmvcfw-controllerName="${controllerParent.constructor.name}"]`); if (!parentContainer) { throw new Error(`@cimo/jsmvcfw - JsMvcFw.ts - renderTrigger() => Tag jsmvcfw-controllerName="${controllerParent.constructor.name}" not found!`); } const elementContainerList = parentContainer.querySelectorAll(`[jsmvcfw-controllerName="${controllerName}"]`); if (!elementContainerList.length) { throw new Error(`@cimo/jsmvcfw - JsMvcFw.ts - renderTrigger() => Tag jsmvcfw-controllerName="${controllerName}" not found inside jsmvcfw-controllerName="${controllerParent.constructor.name}"!`); } let isFirstRenderAtLeastOne = false; elementContainerList.forEach((elementContainer, index) => { const viewAttribute = elementContainer.getAttribute("view"); const viewName = viewAttribute && viewAttribute.trim() !== "" ? viewAttribute.trim() : undefined; let virtualNodeNew = controllerValue.view(viewName); virtualNodeNew = normalizeVirtualNode(virtualNodeNew); if (!virtualNodeNew || typeof virtualNodeNew !== "object" || !virtualNodeNew.tag) { throw new Error(`@cimo/jsmvcfw - JsMvcFw.ts - renderTrigger() => Invalid virtual node returned by controller "${controllerName}"!`); } const slotKey = `${controllerName}::${viewName || "__default__"}::${index}`; const virtualNodeOld = virtualNodeObject[slotKey]; if (!virtualNodeOld) { const elementVirtualNode = createVirtualNode(virtualNodeNew); elementContainer.innerHTML = ""; elementContainer.appendChild(elementVirtualNode); isFirstRenderAtLeastOne = true; } else { const elementFirstChild = elementContainer.firstElementChild; if (elementFirstChild) { updateVirtualNode(elementFirstChild, virtualNodeOld, virtualNodeNew); } } virtualNodeObject[slotKey] = virtualNodeNew; elementHook(elementContainer, controllerValue); }); if (isFirstRenderAtLeastOne && callback) { callback(); } }; renderTriggerObject[controllerName] = renderTrigger; renderTrigger(); if (controllerValue.subControllerList) { const subControllerList = controllerValue.subControllerList(); for (const subController of subControllerList) { renderTemplate(subController, controllerValue, () => { subController.event(); renderAfter(subController).then(() => { subController.rendered(); }); }); } } variableWatch(controllerName, (watch) => { controllerValue.variableEffect.call(controllerValue, watch); }); }; export const renderAfter = (controller) => { return new Promise((resolve) => { const check = () => { const controllerName = controller.constructor.name; if (!variableLoadedList[controllerName]) { resolve(); return; } const isRendering = variableRenderUpdateObject[controllerName]; if (!isRendering) { resolve(); return; } Promise.resolve().then(check); }; check(); }); }; export const variableHook = (label, stateValue, controllerName) => { if (!(controllerName in variableHookObject)) { if (!variableLoadedList[controllerName]) { variableLoadedList[controllerName] = []; variableEditedList[controllerName] = []; } if (variableLoadedList[controllerName].includes(label)) { throw new Error(`@cimo/jsmvcfw - JsMvcFw.ts - variableHook() => The method variableHook use existing label "${label}"!`); } variableLoadedList[controllerName].push(label); variableHookObject[controllerName] = variableProxy(label, stateValue, controllerName); } return { state: variableHookObject[controllerName], setState: (value) => { if (variableEditedList[controllerName] && !variableEditedList[controllerName].includes(label)) { variableEditedList[controllerName].push(label); } variableHookObject[controllerName] = variableProxy(label, value, controllerName); variableRenderUpdate(controllerName); } }; }; export const variableBind = (inputObject, controllerName) => { const result = {}; if (!variableLoadedList[controllerName]) { variableLoadedList[controllerName] = []; variableEditedList[controllerName] = []; } if (!variableBindRegistry[controllerName]) { variableBindRegistry[controllerName] = {}; } for (const key in inputObject) { if (!Object.prototype.hasOwnProperty.call(inputObject, key)) { continue; } if (variableLoadedList[controllerName].includes(key)) { throw new Error(`@cimo/jsmvcfw - JsMvcFw.ts - variableBind() => The method variableBind use existing label "${key}"!`); } variableLoadedList[controllerName].push(key); const keyTyped = key; const target = inputObject[keyTyped]; let initialValue = target; if (variableLinkReference(target)) { const sourceControllerObject = variableBindRegistry[target.controllerNameSource]; const sourceBind = sourceControllerObject ? sourceControllerObject[key] : null; initialValue = sourceBind ? variableLinkClone(sourceBind.state) : undefined; } const bindItem = variableBindItem(key, initialValue, controllerName); result[keyTyped] = bindItem; variableBindRegistry[controllerName][key] = bindItem; } for (const key in inputObject) { if (!Object.prototype.hasOwnProperty.call(inputObject, key)) { continue; } const keyTyped = key; const target = inputObject[keyTyped]; if (!variableLinkReference(target)) { continue; } const targetBind = result[keyTyped]; const isResolved = variableLinkResolve(target, key, targetBind); if (!isResolved) { variableLinkPendingList.push({ target, label: key, targetBind }); } } variableLinkPendingFlush(); return result; }; export const variableLink = (controllerNameSource) => { return { __jsmvcfwType: "variableLink", controllerNameSource }; }; export const elementObserver = (element, callback) => { const callbackList = callbackObserverWeakMap.get(element) || []; callbackObserverWeakMap.set(element, [...callbackList, callback]); if (!observerWeakMap.has(element)) { const observer = new MutationObserver((mutationRecordList) => { const callbackList = callbackObserverWeakMap.get(element); if (!callbackList) { return; } for (const mutationRecord of mutationRecordList) { for (const callback of callbackList) { callback(element, mutationRecord); } } }); observer.observe(element, { subtree: true, childList: true, attributes: true }); observerWeakMap.set(element, observer); } }; export const elementObserverOff = (element) => { const observer = observerWeakMap.get(element); if (observer) { observer.disconnect(); } }; export const elementObserverOn = (element) => { const observer = observerWeakMap.get(element); if (observer) { observer.observe(element, { subtree: true, childList: true, attributes: true }); } }; export const frameworkReset = () => { Object.keys(virtualNodeObject).forEach((key) => delete virtualNodeObject[key]); Object.keys(renderTriggerObject).forEach((key) => delete renderTriggerObject[key]); Object.keys(variableBindRegistry).forEach((key) => delete variableBindRegistry[key]); Object.keys(variableLoadedList).forEach((key) => delete variableLoadedList[key]); Object.keys(variableEditedList).forEach((key) => delete variableEditedList[key]); Object.keys(variableRenderUpdateObject).forEach((key) => delete variableRenderUpdateObject[key]); Object.keys(variableHookObject).forEach((key) => delete variableHookObject[key]); variableLinkPendingList.length = 0; controllerList.length = 0; cacheVariableProxyWeakMap = new WeakMap(); Object.keys(emitterObject).forEach((key) => delete emitterObject[key]); observerWeakMap = new WeakMap(); callbackObserverWeakMap = new WeakMap(); }; //# sourceMappingURL=JsMvcFw.js.map