134 lines
4.3 KiB
JavaScript
134 lines
4.3 KiB
JavaScript
import { HasElementTags, hashTag, normaliseProps, tagDedupeKey, defineHeadPlugin } from '@unhead/shared';
|
|
|
|
async function elementToTag($el) {
|
|
const tag = {
|
|
tag: $el.tagName.toLowerCase(),
|
|
props: await normaliseProps(
|
|
$el.getAttributeNames().reduce((props, name) => ({ ...props, [name]: $el.getAttribute(name) }), {})
|
|
),
|
|
innerHTML: $el.innerHTML
|
|
};
|
|
tag._d = tagDedupeKey(tag);
|
|
return tag;
|
|
}
|
|
async function renderDOMHead(head, options = {}) {
|
|
const dom = options.document || head.resolvedOptions.document;
|
|
if (!dom)
|
|
return;
|
|
const beforeRenderCtx = { shouldRender: head.dirty, tags: [] };
|
|
await head.hooks.callHook("dom:beforeRender", beforeRenderCtx);
|
|
if (!beforeRenderCtx.shouldRender)
|
|
return;
|
|
const tags = (await head.resolveTags()).map((tag) => ({
|
|
tag,
|
|
id: HasElementTags.includes(tag.tag) ? hashTag(tag) : tag.tag,
|
|
shouldRender: true
|
|
}));
|
|
let state = head._dom;
|
|
if (!state) {
|
|
state = {
|
|
elMap: { htmlAttrs: dom.documentElement, bodyAttrs: dom.body }
|
|
};
|
|
for (const key of ["body", "head"]) {
|
|
const children = dom?.[key]?.children;
|
|
for (const c of [...children].filter((c2) => HasElementTags.includes(c2.tagName.toLowerCase())))
|
|
state.elMap[c.getAttribute("data-hid") || hashTag(await elementToTag(c))] = c;
|
|
}
|
|
}
|
|
state.pendingSideEffects = { ...state.sideEffects || {} };
|
|
state.sideEffects = {};
|
|
function track(id, scope, fn) {
|
|
const k = `${id}:${scope}`;
|
|
state.sideEffects[k] = fn;
|
|
delete state.pendingSideEffects[k];
|
|
}
|
|
function trackCtx({ id, $el, tag }) {
|
|
const isAttrTag = tag.tag.endsWith("Attrs");
|
|
state.elMap[id] = $el;
|
|
if (!isAttrTag) {
|
|
["textContent", "innerHTML"].forEach((k) => {
|
|
tag[k] && tag[k] !== $el[k] && ($el[k] = tag[k]);
|
|
});
|
|
track(id, "el", () => {
|
|
state.elMap[id].remove();
|
|
delete state.elMap[id];
|
|
});
|
|
}
|
|
Object.entries(tag.props).forEach(([k, value]) => {
|
|
const ck = `attr:${k}`;
|
|
if (k === "class") {
|
|
for (const c of (value || "").split(" ").filter(Boolean)) {
|
|
isAttrTag && track(id, `${ck}:${c}`, () => $el.classList.remove(c));
|
|
!$el.classList.contains(c) && $el.classList.add(c);
|
|
}
|
|
} else {
|
|
$el.getAttribute(k) !== value && $el.setAttribute(k, value === true ? "" : String(value));
|
|
isAttrTag && track(id, ck, () => $el.removeAttribute(k));
|
|
}
|
|
});
|
|
}
|
|
const pending = [];
|
|
const frag = {
|
|
bodyClose: void 0,
|
|
bodyOpen: void 0,
|
|
head: void 0
|
|
};
|
|
for (const ctx of tags) {
|
|
const { tag, shouldRender, id } = ctx;
|
|
if (!shouldRender)
|
|
continue;
|
|
if (tag.tag === "title") {
|
|
dom.title = tag.textContent;
|
|
continue;
|
|
}
|
|
ctx.$el = ctx.$el || state.elMap[id];
|
|
if (ctx.$el)
|
|
trackCtx(ctx);
|
|
else
|
|
HasElementTags.includes(tag.tag) && pending.push(ctx);
|
|
}
|
|
for (const ctx of pending) {
|
|
const pos = ctx.tag.tagPosition || "head";
|
|
ctx.$el = dom.createElement(ctx.tag.tag);
|
|
trackCtx(ctx);
|
|
frag[pos] = frag[pos] || dom.createDocumentFragment();
|
|
frag[pos].appendChild(ctx.$el);
|
|
}
|
|
for (const ctx of tags)
|
|
await head.hooks.callHook("dom:renderTag", ctx, dom, track);
|
|
frag.head && dom.head.appendChild(frag.head);
|
|
frag.bodyOpen && dom.body.insertBefore(frag.bodyOpen, dom.body.firstChild);
|
|
frag.bodyClose && dom.body.appendChild(frag.bodyClose);
|
|
Object.values(state.pendingSideEffects).forEach((fn) => fn());
|
|
head._dom = state;
|
|
head.dirty = false;
|
|
await head.hooks.callHook("dom:rendered", { renders: tags });
|
|
}
|
|
|
|
async function debouncedRenderDOMHead(head, options = {}) {
|
|
const fn = options.delayFn || ((fn2) => setTimeout(fn2, 10));
|
|
return head._domUpdatePromise = head._domUpdatePromise || new Promise((resolve) => fn(async () => {
|
|
await renderDOMHead(head, options);
|
|
delete head._domUpdatePromise;
|
|
resolve();
|
|
}));
|
|
}
|
|
|
|
// @__NO_SIDE_EFFECTS__
|
|
function DomPlugin(options) {
|
|
return defineHeadPlugin((head) => {
|
|
const initialPayload = head.resolvedOptions.document?.head.querySelector('script[id="unhead:payload"]')?.innerHTML || false;
|
|
initialPayload && head.push(JSON.parse(initialPayload));
|
|
return {
|
|
mode: "client",
|
|
hooks: {
|
|
"entries:updated": function(head2) {
|
|
debouncedRenderDOMHead(head2, options);
|
|
}
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
export { DomPlugin, debouncedRenderDOMHead, renderDOMHead };
|