import {OptionalType, ReflectUtils} from '@dooboostore/core';
import {ensureInit, getElementConfig} from './elementDefine';
import {SwcUtils} from "../utils/Utils";
import {NodeSlot} from "@dooboostore/core-web";
import {HelperHostSet} from "../types";
/*
<-- it's me
foo
*/
export type ApplySlotPosition = 'prepend' | 'prependHtml' | 'prependText' | 'append' | 'appendHtml' | 'appendText' | 'replaceChildren' | 'replaceChildrenHtml' | 'replaceChildrenText' | 'clear';
export interface ApplySlotOptions {
position: ApplySlotPosition;
fallback?: (helper: HelperHostSet) => string | Node;
/**
* Custom key to extract value from return object.
* If not provided, uses APPLY_SLOT_METADATA_KEY by default.
* Useful when multiple @applySlot decorators are on the same method.
*/
valueKey?: symbol | string;
}
export interface ApplySlotMetadata {
propertyKey: string | symbol;
targetId: string;
// type: 'method' | 'property';
options: ApplySlotOptions;
}
export const APPLY_SLOT_METADATA_KEY = Symbol.for('simple-web-component:apply-slot');
// const normalizeNodes = (res: any, doc: Document): Node[] => {
// const items = Array.isArray(res) ? res : [res];
// return items.map(it => {
// if (it instanceof Node) return it;
// return doc.createTextNode(it !== undefined && it !== null ? String(it) : '');
// });
// };
// const applyToDom = (targetEl: any, res: any, pos: ApplyNodePosition, win: Window) => {
// if (!targetEl) return;
//
// const doc = win.document;
//
// if (pos === 'innerHtml') {
// targetEl.innerHTML = res !== undefined && res !== null ? String(res) : '';
// return;
// }
//
// if (pos === 'innerText') {
// if (targetEl.innerText !== undefined) {
// targetEl.innerText = res !== undefined && res !== null ? String(res) : '';
// }
// return;
// }
//
// const isShadowRoot = targetEl instanceof win.ShadowRoot;
// const nodes = normalizeNodes(res, doc);
// if (pos === 'replace') {
// if (!isShadowRoot) targetEl.replaceWith(...nodes);
// } else if (pos === 'replaceChildren') {
// targetEl.replaceChildren(...nodes);
// } else {
// if (pos === 'beforeEnd') targetEl.append(...nodes);
// else if (pos === 'afterBegin') targetEl.prepend(...nodes);
// else if (pos === 'beforeBegin' && !isShadowRoot) targetEl.before(...nodes);
// else if (pos === 'afterEnd' && !isShadowRoot) targetEl.after(...nodes);
// }
// };
const applyToDom = (host: HTMLElement, swcId: string, id: string, pos: ApplySlotPosition, nodes: Node[]) => {
const nodeSlot = new NodeSlot(host, swcId);
if (pos === 'replaceChildrenText' || pos === 'replaceChildren' || pos === 'replaceChildrenHtml') {
nodeSlot.replaceChildren(id, ...nodes);
} else if (pos === 'appendHtml' || pos === 'append' || pos === 'appendText') {
nodeSlot.append(id, ...nodes);
} else if (pos === 'prependHtml' || pos === 'prepend' || pos === 'prependText') {
nodeSlot.prepend(id, ...nodes);
} else if (pos === 'clear') {
nodeSlot.clear(id);
}
}
/**
* @applyNode decorator to surgically add/replace nodes to a target element.
*/
export function applySlot(targetId: string, opt: OptionalType): MethodDecorator {
return (targetObj: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor) => {
const options: ApplySlotOptions = {position: 'replaceChildren', ...opt};
const constructor = targetObj.constructor;
let metaList = ReflectUtils.getOwnMetadata(APPLY_SLOT_METADATA_KEY, constructor) as ApplySlotMetadata[];
if (!metaList) {
metaList = []
ReflectUtils.defineMetadata(APPLY_SLOT_METADATA_KEY, metaList, constructor);
}
metaList.push({propertyKey, targetId, options});
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
ensureInit(this);
const host = this;
const conf = getElementConfig(this);
const currentWin = conf.window;
const swcId = this._swcId;
const currentDoc = currentWin.document;
const hostSet = SwcUtils.getHelperAndHostSet(currentWin, this);
const pos = options.position;
const resToNodes = (res: string | Node) => {
const nodes: Node[] = [];
if (typeof res === 'string' && (pos === 'replaceChildrenHtml' || pos === 'appendHtml' || pos === 'prependHtml')) {
const t = currentWin.document.createElement('template');
t.innerHTML = res;
nodes.push(...Array.from(t.content.childNodes));
} else if (typeof res === 'string' && (pos === 'replaceChildrenText' || pos === 'prependText' || pos === 'appendText')) {
nodes.push(currentWin.document.createTextNode(res));
} else if (typeof res === 'object') {
nodes.push(res);
}
SwcUtils.projectProcessHtml(swcId, nodes, currentDoc);
return nodes;
}
const fallbackNodes: Node[] = [];
if (options.fallback) {
const res: string | Node = options.fallback.call(this, hostSet);
const nodes: Node[] = resToNodes(res);
fallbackNodes.push(...nodes);
applyToDom(host, swcId, targetId, options.position, nodes);
}
/**
* Extract value for this decorator from method return value
*
* If return value is an object with this decorator's key,
* use that value. Otherwise use the entire return value.
*
* Uses valueKey from options if provided, otherwise uses APPLY_SLOT_METADATA_KEY.
*/
const extractValue = (v: any) => {
const keyToUse = options.valueKey ?? APPLY_SLOT_METADATA_KEY;
if (v && typeof v === 'object' && keyToUse in v) {
return v[keyToUse];
}
return v;
};
let res = original.apply(this, args);
if (res instanceof Promise) {
return res.then(asyncRes => {
const extracted = extractValue(asyncRes);
const nodes = resToNodes(extracted)
fallbackNodes.forEach((it: any) => it.remove());
applyToDom(host, swcId, targetId, options.position, nodes);
})
} else {
const extracted = extractValue(res);
const nodes: Node[] = resToNodes(extracted);
applyToDom(host, swcId, targetId, options.position, nodes);
}
};
};
}
export function clearSlot(targetId: string): MethodDecorator {
return applySlot(targetId, {position: 'clear'});
}
export function prependHtmlSlot(targetId: string): MethodDecorator {
return applySlot(targetId, {position: 'prependHtml'});
}
export function prependSlot(targetId: string): MethodDecorator {
return applySlot(targetId, {position: 'prepend'});
}
export function prependTextSlot(targetId: string): MethodDecorator {
return applySlot(targetId, {position: 'prependText'});
}
export function appendHtmlSlot(targetId: string): MethodDecorator {
return applySlot(targetId, {position: 'appendHtml'});
}
export function appendTextSlot(targetId: string): MethodDecorator {
return applySlot(targetId, {position: 'appendText'});
}
export function appendSlot(targetId: string): MethodDecorator {
return applySlot(targetId, {position: 'append'});
}
export function replaceChildrenHtmlSlot(targetId: string): MethodDecorator {
return applySlot(targetId, {position: 'replaceChildrenHtml'});
}
export function replaceChildrenSlot(targetId: string): MethodDecorator {
return applySlot(targetId, {position: 'replaceChildren'});
}
export function replaceChildrenTextSlot(targetId: string): MethodDecorator {
return applySlot(targetId, {position: 'replaceChildrenText'});
}
// export const findAllApplySlotMetadata = (target: any): Map => {
export const findAllApplySlotMetadata = (target: any): ApplySlotMetadata[] => {
const constructor = target instanceof Function ? target : target.constructor;
return ReflectUtils.getMetadata(APPLY_SLOT_METADATA_KEY, constructor);
};