@dooboostore/dom-render
Version:
html view template engine
914 lines (841 loc) • 37.2 kB
text/typescript
import { DomRenderConfig } from '../configs/DomRenderConfig';
import { ScriptUtils } from '@dooboostore/core-web';
import { ElementUtils } from '@dooboostore/core-web';
import { Range } from '@dooboostore/core';
import { getDomRenderConfig } from '../DomRenderProxy';
import { ObjectUtils } from '@dooboostore/core';
type HandlerInfo = {
element: HTMLElement;
attr: string;
type: 'link' | 'event' | 'event-param';
priority: number;
property?: string; // for link type
};
export type NormalAttrDataType = {
originalAttrValue: string;
isStringTemplate: boolean;
variablePaths: { origin: string; inner: string }[];
};
export class EventManager {
public static readonly attrPrefix = 'dr-';
public readonly eventNames = ['click', 'mousedown', 'mouseup', 'dblclick', 'mouseover', 'mouseout', 'mousemove', 'mouseenter', 'mouseleave', 'contextmenu', 'keyup', 'keydown', 'keypress', 'toggle', 'change', 'input', 'submit', 'resize', 'focus', 'blur', 'close', 'cancel', 'scroll'];
private readonly delegatableEventMap: { [key: string]: string } = {
mouseleave: 'mouseout',
mouseenter: 'mouseover',
focus: 'focusin',
blur: 'focusout'
};
private readonly directAttachEvents = new Set(['close', 'scroll']);
public static ownerVariablePathAttrName = EventManager.attrPrefix + 'owner-variable-path';
public static readonly eventParam = EventManager.attrPrefix + 'event';
public static readonly onInitAttrName = EventManager.attrPrefix + 'on-init';
public static readonly valueAttrName = EventManager.attrPrefix + 'value';
public static readonly srcAttrName = EventManager.attrPrefix + 'src';
public static readonly checkedAttrName = EventManager.attrPrefix + 'checked';
public static readonly selectedAttrName = EventManager.attrPrefix + 'selected';
public static readonly readonlyAttrName = EventManager.attrPrefix + 'readonly';
public static readonly disabledAttrName = EventManager.attrPrefix + 'disabled';
public static readonly hiddenAttrName = EventManager.attrPrefix + 'hidden';
public static readonly requiredAttrName = EventManager.attrPrefix + 'required';
public static readonly openAttrName = EventManager.attrPrefix + 'open';
// link는 쓰지 않는걸 추천한다 라이프 사이클 꼬인다 다른 프레임워크에서도 양방향은 지양 한다
public static linkAttrs = [
{ name: EventManager.attrPrefix + 'value-link', property: 'value', event: 'input' },
{ name: EventManager.attrPrefix + 'hidden-link', property: 'value', event: 'input' },
{ name: EventManager.attrPrefix + 'required-link', property: 'value', event: 'input' },
{ name: EventManager.attrPrefix + 'checked-link', property: 'checked', event: 'change' },
{ name: EventManager.attrPrefix + 'open-link', property: 'open', event: 'toggle' }
];
// @ts-ignore 합성이벤트(Synthetic) 처리하면서 의미가 없어졌다
public static readonly linkTargetMapAttrName = EventManager.attrPrefix + 'link-variables';
public static readonly onRenderedInitAttrName = EventManager.attrPrefix + 'on-rendered-init';
public static readonly attrAttrName = EventManager.attrPrefix + 'attr';
public static readonly normalAttrMapAttrName = EventManager.attrPrefix + 'normal-attr-map';
public static readonly styleAttrName = EventManager.attrPrefix + 'style';
public static readonly classAttrName = EventManager.attrPrefix + 'class';
public static readonly VALUE_VARNAME = '$value';
public static readonly SCRIPTS_VARNAME = '$scripts';
public static readonly FAG_VARNAME = '$fag';
public static readonly RAWSET_VARNAME = '$rawSet';
public static readonly RENDER_VARNAME = '$render';
public static readonly NEAR_THIS_VARNAME = '$nearThis';
public static readonly PARENT_THIS_VARNAME = '$parentThis';
public static readonly ROOT_OBJECT_VARNAME = '$rootObject';
public static readonly SCRIPT_UTILS_VARNAME = '$scriptUtils';
public static readonly RANGE_VARNAME = '$range';
public static readonly ROUTER_VARNAME = '$router';
public static readonly INNER_HTML_VARNAME = '$innerHTML';
public static readonly ELEMENT_VARNAME = '$element';
public static readonly TARGET_VARNAME = '$target';
public static readonly EVENT_VARNAME = '$event';
public static readonly COMPONENT_VARNAME = '$component';
public static readonly INNERHTML_VARNAME = '$innerHTML';
public static readonly ATTRIBUTE_VARNAME = '$attribute';
public static readonly CREATOR_META_DATA_VARNAME = '$creatorMetaData';
public static readonly PARENT_THIS_PATH_VARNAME = '$parentPath';
public static readonly CURRENT_THIS_VARNAME = '$currentThis';
public static readonly CURRENT_THIS_PATH_VARNAME = '$currentThisPath';
public static readonly NEAR_THIS_PATH_VARNAME = '$nearThisPath';
public static readonly VARNAMES = [EventManager.SCRIPTS_VARNAME, EventManager.FAG_VARNAME, EventManager.RAWSET_VARNAME, EventManager.RANGE_VARNAME, EventManager.ROUTER_VARNAME, EventManager.ELEMENT_VARNAME, EventManager.TARGET_VARNAME, EventManager.EVENT_VARNAME, EventManager.COMPONENT_VARNAME, EventManager.INNERHTML_VARNAME, EventManager.ATTRIBUTE_VARNAME, EventManager.ATTRIBUTE_VARNAME, EventManager.CREATOR_META_DATA_VARNAME, EventManager.PARENT_THIS_PATH_VARNAME, EventManager.PARENT_THIS_VARNAME];
public static readonly WINDOW_EVENT_POPSTATE = 'popstate';
public static readonly WINDOW_EVENT_RESIZE = 'resize';
public static readonly WINDOW_EVENT_SCROLL = 'scroll';
public static readonly WINDOW_EVENTS = [EventManager.WINDOW_EVENT_POPSTATE, EventManager.WINDOW_EVENT_RESIZE, EventManager.WINDOW_EVENT_SCROLL];
public static readonly noDetectAttr = [EventManager.normalAttrMapAttrName];
public static readonly attrNames = [EventManager.valueAttrName, EventManager.srcAttrName, EventManager.checkedAttrName, EventManager.selectedAttrName, EventManager.readonlyAttrName, EventManager.disabledAttrName, EventManager.hiddenAttrName, EventManager.requiredAttrName, EventManager.openAttrName, EventManager.attrAttrName, EventManager.normalAttrMapAttrName, EventManager.styleAttrName, EventManager.classAttrName, EventManager.attrPrefix + 'window-event-' + EventManager.WINDOW_EVENT_POPSTATE, EventManager.attrPrefix + 'window-event-' + EventManager.WINDOW_EVENT_RESIZE, EventManager.attrPrefix + 'window-event-' + EventManager.WINDOW_EVENT_SCROLL, EventManager.onInitAttrName, ...EventManager.linkAttrs.map(it => it.name), this.eventParam];
readonly bindScript = `
const ${EventManager.VALUE_VARNAME} = this.__render.value;
const ${EventManager.SCRIPTS_VARNAME} = this.__render.scripts;
const ${EventManager.RANGE_VARNAME} = this.__render.range;
const ${EventManager.ROUTER_VARNAME} = this.__render.router;
const ${EventManager.ATTRIBUTE_VARNAME} = this.__render.attribute;
const ${EventManager.ELEMENT_VARNAME} = this.__render.element;
const ${EventManager.COMPONENT_VARNAME} = this.__render.element.component;
const ${EventManager.TARGET_VARNAME} = this.__render.target;
const ${EventManager.EVENT_VARNAME} = this.__render.event;
`;
constructor(private window: Window) {
this.eventNames.forEach(it => {
EventManager.attrNames.push(EventManager.attrPrefix + 'event-' + it);
});
EventManager.attrNames.push(EventManager.onRenderedInitAttrName);
if (typeof window !== 'undefined') {
EventManager.WINDOW_EVENTS.forEach(eventName => {
window?.addEventListener(eventName, event => {
const targetAttr = `dr-window-event-${eventName}`;
document.querySelectorAll(`[${targetAttr}]`).forEach(it => {
const script = it.getAttribute(targetAttr);
if (script) {
const obj = this.findComponentInstance(it as HTMLElement);
const config = getDomRenderConfig(obj);
ScriptUtils.evaluate(`${this.bindScript} ${script} `, this.createExecutionContext(event, it as HTMLElement, obj, config));
}
});
});
});
const rootElement = this.window.document;
const listeningEvents = new Set<string>();
this.eventNames.forEach(eventName => {
if (this.directAttachEvents.has(eventName)) {
return;
}
const delegatedEventName = this.delegatableEventMap[eventName] || eventName;
if (listeningEvents.has(delegatedEventName)) {
return;
}
listeningEvents.add(delegatedEventName);
rootElement.addEventListener(delegatedEventName, this.handleEvent.bind(this), true);
});
}
}
private handleEvent(event: Event) {
const handlers = this.collectHandlers(event);
// console.log('handlers', handlers, event);
for (const handler of handlers) {
if (event.cancelBubble) {
break;
}
const componentInstance = this.findComponentInstance(handler.element);
if (!componentInstance) {
continue;
}
// console.log('executeHandler', event,handler,componentInstance)
this.executeHandler(event, handler, componentInstance);
}
}
private collectHandlers(event: Event): HandlerInfo[] {
const handlers: HandlerInfo[] = [];
const root = this.window.document;
if (!root) return handlers;
const initialTarget = event.target as Node;
let currentTarget: HTMLElement | null = initialTarget.nodeType === 1 ? (initialTarget as HTMLElement) : initialTarget.parentElement;
const collectedElements = new Set<HTMLElement>();
const eventMap: { [key: string]: string } = {
mouseout: 'mouseleave',
mouseover: 'mouseenter',
focusout: 'blur',
focusin: 'focus'
};
const mappedEventName = eventMap[event.type] || event.type;
while (currentTarget) {
if (collectedElements.has(currentTarget)) {
break;
}
// Priority 1: Link attributes
const matchingLinkAttr = EventManager.linkAttrs.find(la => la.event === mappedEventName && currentTarget?.hasAttribute(la.name));
if (matchingLinkAttr) {
handlers.push({
element: currentTarget,
attr: matchingLinkAttr.name,
type: 'link',
priority: 1,
property: matchingLinkAttr.property
});
}
// Priority 2: dr-event-* attributes
const eventAttr = `${EventManager.attrPrefix}event-${mappedEventName}`;
if (currentTarget.hasAttribute(eventAttr)) {
handlers.push({
element: currentTarget,
attr: eventAttr,
type: 'event',
priority: 2
});
}
// Priority 3: dr-event with params
const eventParamAttr = EventManager.eventParam;
if (currentTarget.hasAttribute(eventParamAttr)) {
const bindEvents = currentTarget.getAttribute(`${eventParamAttr}:bind`);
if (
bindEvents &&
bindEvents
.split(',')
.map(s => s.trim())
.includes(mappedEventName)
) {
handlers.push({
element: currentTarget,
attr: eventParamAttr,
type: 'event-param',
priority: 3
});
}
}
collectedElements.add(currentTarget);
if (currentTarget === root.documentElement) {
break;
}
currentTarget = currentTarget.parentElement;
}
return handlers.sort((a, b) => a.priority - b.priority);
}
private findComponentInstance(element: HTMLElement | null): any {
let current = element;
const root = this.window.document;
while (current) {
if ((current as any).obj) {
return (current as any).obj;
}
if (current === root.documentElement) {
break;
}
current = current.parentElement;
}
return this.window;
}
private createExecutionContext(event: Event, element: HTMLElement, componentInstance: any, config: DomRenderConfig | undefined, extraVars = {}) {
// console.log('---------->', config?.eventVariables)
return Object.assign(componentInstance, {
__render: Object.freeze({
event,
element,
target: event.target,
attribute: ElementUtils.getAttributeToObject(element),
router: config?.router,
range: Range.range,
scripts: EventManager.setBindProperty(config?.scripts, componentInstance),
...config?.eventVariables,
...extraVars
})
});
}
private executeHandler(event: Event, handler: HandlerInfo, componentInstance: any) {
const { element, type, attr } = handler;
const config = getDomRenderConfig(componentInstance);
switch (type) {
case 'link':
const varName = element.getAttribute(attr);
if (!varName || !handler.property) return;
if (attr.includes('leave') || attr.includes('enter') || attr.includes('focus') || attr.includes('blur')) {
const relatedTarget = (event as MouseEvent).relatedTarget as Node;
if (relatedTarget && element.contains(relatedTarget)) {
return;
}
}
const ownerVariablePath = element.getAttribute(EventManager.ownerVariablePathAttrName);
let bindObj = componentInstance;
if (ownerVariablePath) {
bindObj = ScriptUtils.evaluateReturn(ownerVariablePath, componentInstance);
}
const mapScript = element.getAttribute(`${attr}:map`);
let value = (element as any)[handler.property];
if (mapScript) {
const context = this.createExecutionContext(event, element, bindObj, config, { value });
value = ScriptUtils.evaluate(`return ${mapScript}`, context);
}
if (typeof this.getValue(componentInstance, varName, bindObj) === 'function') {
this.getValue(componentInstance, varName, bindObj)(value, event);
} else {
this.setValue(componentInstance, varName, value);
}
break;
case 'event':
const script = element.getAttribute(attr);
if (!script) return;
if (attr.endsWith('mouseleave') || attr.endsWith('mouseenter') || attr.endsWith('blur') || attr.endsWith('focus')) {
const relatedTarget = (event as MouseEvent).relatedTarget as Node;
if (relatedTarget && element.contains(relatedTarget)) {
return;
}
}
const context = this.createExecutionContext(event, element, componentInstance, config);
const filterScript = element.getAttribute(`${attr}:filter`);
let filter = true;
// 개발자가 사용중에 문제가 발생될(디버깅) 버그를 유발할수있는 기능 흐름제어가 안될수있어 기능을 제거한다.
// if (filterScript) {
// filter = ScriptUtils.eval<boolean>(`${this.getBindScript(config)} return ${filterScript}`, context) ?? false;
// }
if (filter) {
ScriptUtils.evaluate(`${this.getBindScript(config)} ${script}`, context);
}
// 개발자가 사용중에 문제가 발생될(디버깅) 버그를 유발할수있는 기능 흐름제어가 안될수있어 기능을 제거한다.
// const hasDispatch = element.hasAttribute(`${attr}:dispatch`);
// if (hasDispatch) {
// element.dispatchEvent(new Event(event.type));
// }
break;
case 'event-param':
const paramScript = element.getAttribute(attr);
if (!paramScript) return;
if (attr.endsWith('mouseleave') || attr.endsWith('mouseenter') || attr.endsWith('blur') || attr.endsWith('focus')) {
const relatedTarget = (event as MouseEvent).relatedTarget as Node;
if (relatedTarget && element.contains(relatedTarget)) {
return;
}
}
const attributes = ElementUtils.getAttributeToObject(element);
const params = {} as any;
const prefix = attr + ':';
Object.entries(attributes)
.filter(([k, v]) => k.startsWith(prefix))
.forEach(([k, v]) => {
params[k.slice(prefix.length)] = v;
});
const paramContext = this.createExecutionContext(event, element, componentInstance, config, { params });
ScriptUtils.evaluate(`${this.getBindScript(config)} ${paramScript}`, paramContext);
break;
}
}
public findAttrElements(fragment: DocumentFragment | Element, config?: DomRenderConfig): Set<Element> {
// const t0 = performance.now();
const elements = new Set<Element>();
const addAttributes = config?.applyEvents?.map(it => it.attrName) ?? [];
const attrs = addAttributes.concat([...EventManager.attrNames]).filter(Boolean);
if (attrs.length) {
const selector = attrs.map(a => `[${a}]`).join(',');
fragment?.querySelectorAll(selector).forEach(it => elements.add(it));
}
// console.log('findAttrElements time', performance.now() - t0);
return elements;
}
public applyEvent(obj: any, childNodes: Set<ChildNode> | Set<Element>, config?: DomRenderConfig) {
childNodes.forEach(node => {
if (node.nodeType === 1) {
(node as any).obj = obj;
}
});
// let t0 = performance.now();
this.procAttr<HTMLInputElement>(childNodes, EventManager.valueAttrName, (it, attribute) => {
const script = attribute;
if (script) {
const data = ObjectUtils.Script.evaluateReturn(script, obj);
if (it.value !== data) {
it.value = data;
}
}
});
this.procAttr<HTMLInputElement>(childNodes, EventManager.srcAttrName, (it, attribute) => {
const script = attribute;
if (script) {
const data = ObjectUtils.Script.evaluateReturn(script, obj);
if (it.src !== data) {
it.src = data;
}
}
});
// console.log('applyEvent value time', performance.now() - t0);
// t0 = performance.now();
this.procAttr<HTMLInputElement>(childNodes, EventManager.checkedAttrName, (it, attribute) => {
const script = attribute;
if (script) {
const data = ObjectUtils.Script.evaluateReturn(script, obj);
if (it.checked !== data) {
it.checked = data;
}
}
});
// console.log('applyEvent value time', performance.now() - t0);
// t0 = performance.now();
this.procAttr<HTMLOptionElement>(childNodes, EventManager.selectedAttrName, (it, attribute) => {
const script = attribute;
if (script) {
const data = ObjectUtils.Script.evaluateReturn(script, obj);
if (data) {
it.setAttribute('selected', 'selected');
} else {
it.removeAttribute('selected');
}
if (it.selected !== data) {
it.selected = data;
}
}
});
// console.log('applyEvent value time', performance.now() - t0);
// t0 = performance.now();
this.procAttr<HTMLInputElement>(childNodes, EventManager.readonlyAttrName, (it, attribute) => {
const script = attribute;
if (script) {
const data = ObjectUtils.Script.evaluateReturn(script, obj);
if (data) {
it.setAttribute('readonly', 'readonly');
} else {
it.removeAttribute('readonly');
}
if (it.readOnly !== data) {
it.readOnly = data;
}
}
});
// console.log('applyEvent value time', performance.now() - t0);
// t0 = performance.now();
this.procAttr<HTMLInputElement>(childNodes, EventManager.disabledAttrName, (it, attribute) => {
const script = attribute;
if (script) {
const data = ObjectUtils.Script.evaluateReturn(script, obj);
if (data) {
it.setAttribute('disabled', 'disabled');
} else {
it.removeAttribute('disabled');
}
if (it.disabled !== data) {
it.disabled = data;
}
}
});
// console.log('applyEvent value time', performance.now() - t0);
// t0 = performance.now();
this.procAttr<HTMLElement>(childNodes, EventManager.hiddenAttrName, (it, attribute) => {
const script = attribute;
if (script) {
const data = ObjectUtils.Script.evaluateReturn(script, obj);
if (data) {
it.setAttribute('hidden', 'hidden');
} else {
it.removeAttribute('hidden');
}
if (it.hidden !== data) {
it.hidden = data;
}
}
});
// console.log('applyEvent value time', performance.now() - t0);
// t0 = performance.now();
this.procAttr<HTMLInputElement>(childNodes, EventManager.requiredAttrName, (it, attribute) => {
const script = attribute;
if (script) {
const data = ObjectUtils.Script.evaluateReturn(script, obj);
if (data) {
it.setAttribute('required', 'required');
} else {
it.removeAttribute('required');
}
if (it.required !== data) {
it.required = data;
}
}
});
// console.log('applyEvent value time', performance.now() - t0);
// t0 = performance.now();
this.procAttr<HTMLDialogElement>(childNodes, EventManager.openAttrName, (it, attribute) => {
const script = attribute;
if (script) {
const data = ObjectUtils.Script.evaluateReturn(script, obj);
if (data) {
it.setAttribute('open', 'open');
} else {
it.removeAttribute('open');
}
if (it.open !== data) {
it.open = data;
}
}
});
// console.log('applyEvent value time', performance.now() - t0);
// t0 = performance.now();
this.procAttr(childNodes, EventManager.normalAttrMapAttrName, (it, attribute) => {
const map = new Map<string, NormalAttrDataType>(JSON.parse(attribute));
map.forEach((v, k) => {
const variablePaths = v.variablePaths;
let targetScript = v.originalAttrValue; //typeof v.originalAttrValue === 'string' ? JSON.parse(v.originalAttrValue) : v.originalAttrValue;
let script: string;
if (v.isStringTemplate) {
variablePaths.forEach(it => {
let r = ObjectUtils.Path.toOptionalChainPath(it.inner);
targetScript = targetScript.replaceAll(it.origin, `\${${r}}`);
});
script = '`' + targetScript + '`';
} else {
variablePaths.forEach(it => {
let r = ObjectUtils.Path.toOptionalChainPath(it.inner);
targetScript = targetScript.replaceAll(it.origin, `${r}`);
});
script = targetScript;
}
const data = ObjectUtils.Script.evaluate(
`const $element = this.element; return ${script}`,
Object.assign(obj, {
__render: Object.freeze({
element: it,
attribute: ElementUtils.getAttributeToObject(it)
})
})
);
// console.log('!!!!!!!!!!!', k, data, script, variablePaths, v)
if (data === null || data === undefined) {
it.removeAttribute(k);
} else {
it.setAttribute(k, data);
}
});
});
// console.log('applyEvent value time', performance.now() - t0);
// t0 = performance.now();
EventManager.WINDOW_EVENTS.forEach(it => {
this.procAttr<HTMLInputElement>(childNodes, EventManager.attrPrefix + 'window-event-' + it, (it, attribute) => {
(it as any).obj = obj;
});
});
// console.log('applyEvent value time', performance.now() - t0);
// t0 = performance.now();
EventManager.linkAttrs.forEach(linkInfo => {
this.procAttr<HTMLElement>(childNodes, linkInfo.name, (it, varName) => {
if (varName) {
const ownerVariablePathName = it.getAttribute(EventManager.ownerVariablePathAttrName);
const mapScript = it.getAttribute(`${linkInfo.name}:map`);
let bindObj = obj;
if (ownerVariablePathName) {
bindObj = ObjectUtils.Script.evaluateReturn(ownerVariablePathName, obj);
}
const getValue = this.getValue(obj, varName, bindObj);
if (typeof getValue === 'function' && getValue) {
let setValue = (it as any)[linkInfo.property];
if (mapScript) {
setValue = ObjectUtils.Script.evaluate(`${this.getBindScript(config)} return ${mapScript}`, Object.assign(bindObj, { __render: Object.freeze({ element: it, target: bindObj, range: Range.range, value: setValue, scripts: EventManager.setBindProperty(config?.scripts, obj), ...config?.eventVariables }) }));
}
getValue(setValue);
} else if (getValue) {
let setValue = getValue;
if (mapScript) {
setValue = ObjectUtils.Script.evaluate(`${this.getBindScript(config)} return ${mapScript}`, Object.assign(bindObj, { __render: Object.freeze({ element: it, target: bindObj, range: Range.range, value: setValue, scripts: EventManager.setBindProperty(config?.scripts, obj), ...config?.eventVariables }) }));
}
if (setValue === null) {
it.removeAttribute(linkInfo.property);
} else {
(it as any)[linkInfo.property] = setValue;
}
} else if (getValue === undefined) {
this.setValue(obj, varName, (it as any)[linkInfo.property]);
}
}
});
});
// console.log('applyEvent value time', performance.now() - t0);
// t0 = performance.now();
this.directAttachEvents.forEach(eventName => {
const attr = `${EventManager.attrPrefix}event-${eventName}`;
this.procAttr<HTMLElement>(childNodes, attr, (element, script) => {
const handler = (event: Event) => {
const componentInstance = this.findComponentInstance(element);
if (!componentInstance) return;
const context = this.createExecutionContext(event, element, componentInstance, config);
const filterScript = element.getAttribute(`${attr}:filter`);
let filter = true;
if (filterScript) {
filter = ObjectUtils.Script.evaluate<boolean>(`${this.getBindScript(config)} return ${filterScript}`, context) ?? false;
}
if (filter) {
ObjectUtils.Script.evaluate(`${this.getBindScript(config)} ${script}`, context);
}
};
element.addEventListener(eventName, handler);
// TODO: Add logic to remove this listener when the element is destroyed.
});
});
// console.log('applyEvent value time', performance.now() - t0);
// t0 = performance.now();
this.procAttr<HTMLElement>(childNodes, EventManager.onInitAttrName, (it, attribute) => {
let script = attribute;
if (script) {
script = 'return ' + script;
}
if (script) {
ObjectUtils.Script.evaluate(
`${this.getBindScript(config)}; ${script} `,
Object.assign(obj, {
__render: Object.freeze({
element: it,
attribute: ElementUtils.getAttributeToObject(it),
...config?.eventVariables
})
})
);
}
});
// console.log('applyEvent value time', performance.now() - t0);
// t0 = performance.now();
this.onRenderedEvent(obj, childNodes, config);
this.changeVar(obj, childNodes, undefined, config);
const elements = Array.from(childNodes)
.filter(it => it.nodeType === 1)
.map(it => it as Element);
elements.forEach(it => {
config?.applyEvents?.filter(ta => it.getAttribute(ta.attrName) !== null).forEach(ta => ta.callBack(it, it.getAttribute(ta.attrName)!, obj));
});
}
public onRenderedEvent(obj: any, childNodes: Set<ChildNode> | Set<Element> | Node[], config?: DomRenderConfig) {
this.procAttr<HTMLInputElement>(childNodes, EventManager.onRenderedInitAttrName, (it, attribute) => {
if (!it.isConnected) {
return;
}
let script = attribute;
if (script) {
script = 'return ' + script;
}
if (script) {
ObjectUtils.Script.evaluate(
`${this.getBindScript(config)}; ${script} `,
Object.assign(obj, {
__render: Object.freeze({
element: it,
attribute: ElementUtils.getAttributeToObject(it),
...config?.eventVariables
})
})
);
}
});
}
public changeVar(obj: any, elements: Set<Element> | Set<ChildNode>, varName?: string, config?: DomRenderConfig) {
// 아래 이부분을 적용해야될지 말아야될지....성능이슈가.. 그래서 우선 timeout으로 테스크큐로.. 우선..
// console.log('changeVar', obj, elements, varName)
this.procAttr(elements, EventManager.styleAttrName, (it: HTMLElement, attribute) => {
let script = attribute;
if (script) {
script = 'return ' + script;
}
if (EventManager.isUsingThisVar(script, varName) || varName === undefined) {
const data = ObjectUtils.Script.evaluate(
`const $element = this.__render.element; ${script} `,
Object.assign(obj, {
__render: Object.freeze({
element: it,
attribute: ElementUtils.getAttributeToObject(it)
})
})
);
if (typeof data === 'string') {
data.split(';').forEach(sit => {
const [key, value] = sit.split(':').map(s => s.trim());
if (key && value) {
(it.style as any)[key] = value;
}
});
// it.setAttribute('style', data);
} else if (Array.isArray(data)) {
data.forEach(sit => {
const [key, value] = sit.split(':').map(s => s.trim());
if (key && value) {
(it.style as any)[key] = value;
}
});
// it.setAttribute('style', data.join(';'));
} else if (data) {
for (const [key, value] of Object.entries(data)) {
// if (it instanceof HTMLElement) {
(it.style as any)[key] = value === null ? null : String(value);
// }
}
}
}
});
this.procAttr(elements, EventManager.classAttrName, (it, attribute) => {
let script = attribute;
if (script) {
script = 'return ' + script;
}
if (EventManager.isUsingThisVar(script, varName) || varName === undefined) {
const data = ObjectUtils.Script.evaluate(
`const $element = this.element; ${script} `,
Object.assign(obj, {
__render: Object.freeze({
element: it,
attribute: ElementUtils.getAttributeToObject(it)
})
})
);
// alert(1)
if (typeof data === 'string') {
data
.split(' ')
.map(it => it.trim())
.filter(it => it.length > 0)
.forEach(cit => it.classList.add(cit.trim()));
} else if (Array.isArray(data)) {
data
.map(it => it.trim())
.filter(it => it.length > 0)
.forEach(cit => it.classList.add(cit.trim()));
} else if (data) {
for (const [key, value] of Object.entries(data)) {
if (it instanceof HTMLElement) {
const v = key.trim();
if (value && v) {
it.classList.add(v);
} else if (v) {
it.classList.remove(v);
}
}
}
}
}
});
this.procAttr<HTMLInputElement>(elements, EventManager.valueAttrName, (it, attribute) => {
if (EventManager.isUsingThisVar(attribute, varName) || varName === undefined) {
const data = ObjectUtils.Script.evaluateReturn(attribute, obj);
if (it.value !== data) it.value = data;
}
});
this.procAttr<HTMLInputElement>(elements, EventManager.checkedAttrName, (it, attribute) => {
if (EventManager.isUsingThisVar(attribute, varName) || varName === undefined) {
const data = ObjectUtils.Script.evaluateReturn(attribute, obj);
if (it.checked !== data) it.checked = data;
}
});
this.procAttr<HTMLOptionElement>(elements, EventManager.selectedAttrName, (it, attribute) => {
if (EventManager.isUsingThisVar(attribute, varName) || varName === undefined) {
const data = ObjectUtils.Script.evaluateReturn(attribute, obj);
if (it.selected !== data) it.selected = data;
}
});
this.procAttr<HTMLInputElement>(elements, EventManager.readonlyAttrName, (it, attribute) => {
if (EventManager.isUsingThisVar(attribute, varName) || varName === undefined) {
const data = ObjectUtils.Script.evaluateReturn(attribute, obj);
if (it.readOnly !== data) it.readOnly = data;
}
});
this.procAttr<HTMLInputElement>(elements, EventManager.disabledAttrName, (it, attribute) => {
if (EventManager.isUsingThisVar(attribute, varName) || varName === undefined) {
const data = ObjectUtils.Script.evaluateReturn(attribute, obj);
if (it.disabled !== data) it.disabled = data;
}
});
this.procAttr<HTMLElement>(elements, EventManager.hiddenAttrName, (it, attribute) => {
if (EventManager.isUsingThisVar(attribute, varName) || varName === undefined) {
const data = ObjectUtils.Script.evaluateReturn(attribute, obj);
if (it.hidden !== data) it.hidden = data;
}
});
this.procAttr<HTMLInputElement>(elements, EventManager.requiredAttrName, (it, attribute) => {
if (EventManager.isUsingThisVar(attribute, varName) || varName === undefined) {
const data = ObjectUtils.Script.evaluateReturn(attribute, obj);
if (it.required !== data) it.required = data;
}
});
this.procAttr<HTMLDialogElement>(elements, EventManager.openAttrName, (it, attribute) => {
if (EventManager.isUsingThisVar(attribute, varName) || varName === undefined) {
const data = ObjectUtils.Script.evaluateReturn(attribute, obj);
if (it.open !== data) it.open = data;
}
});
}
public deepAttributeElements(elements: Set<Element> | Set<ChildNode> | Node[] = new Set(), attrName: string): Set<Element> {
const sets = new Set<Element>();
elements.forEach(it => {
if (!it) {
return;
}
if (it.nodeType === 1) {
const e = it as Element;
if (e.hasAttribute(attrName)) {
sets.add(e);
}
e.querySelectorAll<Element>(`[${attrName}]`).forEach(it => {
sets.add(it);
});
}
});
return sets;
}
public procAttr<T extends Element = Element>(elements: Set<Element> | Set<ChildNode> | Node[] = new Set(), attrName: string, callBack: (h: T, value: string, attributes: any) => void) {
const sets = this.deepAttributeElements(elements, attrName);
sets.forEach(it => {
const attr = it.getAttribute(attrName);
const attrs = ElementUtils.getAttributeToObject(it);
if (attr) {
// setTimeout(() => {
callBack(it as T, attr, attrs);
// }, 0)
}
});
}
public getValue<T = any>(obj: any, name: string, bindObj?: any): T {
let r = ObjectUtils.Script.evaluateReturn(name, obj);
if (typeof r === 'function') {
r = r.bind(bindObj ?? obj);
}
return r;
}
public setValue(obj: any, name: string, value?: any) {
name = name.replaceAll('this.', 'this.this.');
ObjectUtils.Script.evaluate(`${name} = this.value;`, {
this: obj,
value: value
});
}
public static decodeScriptVar(raws: string | null | undefined): string | undefined {
if (raws) {
// TODO: 훔.. 꼭필요한가..? 트리거될때 스크립트변수 까지 감지해야될까?
EventManager.VARNAMES.forEach(it => {
// raws = raws!.replace(RegExp(it.replace('$', '\\$'), 'g'), `this?.___${it}`);
raws = raws!.replace(RegExp(it.replace('$', '\\$'), 'g'), `this.___${it}`);
});
return raws;
}
return undefined;
}
public static isUsingThisVar(raws: string | null | undefined, varName: string | null | undefined): boolean {
raws = EventManager.decodeScriptVar(raws);
if (varName && raws) {
if (varName.startsWith('this.')) {
varName = varName.replace(/this\./, '');
}
const variablePaths = ObjectUtils.Path.detectPathFromScript(raws, { excludeThis: true });
return variablePaths.has(varName);
}
return false;
}
public static setBindProperty(scripts: { [p: string]: any } | undefined, obj: any): { [p: string]: any } | undefined {
if (scripts) {
const newScripts = Object.assign({}, scripts);
for (const [key, value] of Object.entries(newScripts)) {
if (typeof value === 'function') {
newScripts[key] = value.bind(obj);
}
}
return newScripts;
}
}
getBindScript(config?: DomRenderConfig) {
if (config?.eventVariables) {
const bindScript = Object.entries(config.eventVariables)
.filter(([key, value]) => !this.bindScript.includes(`const ${key}`))
.map(([key, value]) => {
return `const ${key} = this.__render.${key};`;
})
.join(';');
return this.bindScript + '' + bindScript;
} else {
return this.bindScript;
}
}
}