@unhead/dom 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +38 -0
- package/dist/index.mjs +104 -0
- package/package.json +41 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { HeadClient, HeadTag, HookResult } from '@unhead/schema';
|
|
2
|
+
|
|
3
|
+
interface DomRenderTagContext {
|
|
4
|
+
head: HeadClient;
|
|
5
|
+
tag: HeadTag;
|
|
6
|
+
document: Document;
|
|
7
|
+
}
|
|
8
|
+
declare module '@unhead/schema' {
|
|
9
|
+
interface HeadHooks {
|
|
10
|
+
'dom:beforeRender': (ctx: {
|
|
11
|
+
head: HeadClient;
|
|
12
|
+
tags: HeadTag[];
|
|
13
|
+
document: Document;
|
|
14
|
+
}) => HookResult;
|
|
15
|
+
'dom:renderTag': (ctx: DomRenderTagContext) => HookResult;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface RenderDomHeadOptions {
|
|
20
|
+
/**
|
|
21
|
+
* Document to use for rendering. Allows stubbing for testing.
|
|
22
|
+
*/
|
|
23
|
+
document?: Document;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Render the head tags to the DOM.
|
|
27
|
+
*/
|
|
28
|
+
declare function renderDOMHead<T extends HeadClient<any>>(head: T, options?: RenderDomHeadOptions): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Global instance of the dom update promise. Used for debounding head updates.
|
|
31
|
+
*/
|
|
32
|
+
declare let domUpdatePromise: Promise<void> | null;
|
|
33
|
+
/**
|
|
34
|
+
* Queue a debounced update of the DOM head.
|
|
35
|
+
*/
|
|
36
|
+
declare function debouncedRenderDOMHead<T extends HeadClient<any>>(delayedFn: (fn: () => void) => void, head: T, options?: RenderDomHeadOptions): Promise<void>;
|
|
37
|
+
|
|
38
|
+
export { DomRenderTagContext, RenderDomHeadOptions, debouncedRenderDOMHead, domUpdatePromise, renderDOMHead };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const TagsWithInnerContent = ["script", "style", "noscript"];
|
|
2
|
+
|
|
3
|
+
const createElement = (tag, document) => {
|
|
4
|
+
const $el = document.createElement(tag.tag);
|
|
5
|
+
for (const k in tag.props)
|
|
6
|
+
$el.setAttribute(k, String(tag.props[k]));
|
|
7
|
+
if (tag.children)
|
|
8
|
+
$el.innerHTML = tag.children;
|
|
9
|
+
return $el;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function setAttributesWithSideEffects(head, $el, entry, tag) {
|
|
13
|
+
const attrs = tag.props || {};
|
|
14
|
+
const sdeKey = `${tag._p}:attr`;
|
|
15
|
+
Object.entries(entry._sde).filter(([key]) => key.startsWith(sdeKey)).forEach(([key, fn]) => {
|
|
16
|
+
delete entry._sde[key] && fn();
|
|
17
|
+
});
|
|
18
|
+
Object.entries(attrs).forEach(([k, value]) => {
|
|
19
|
+
value = String(value);
|
|
20
|
+
const attrSdeKey = `${sdeKey}:${k}`;
|
|
21
|
+
head._removeQueuedSideEffect(attrSdeKey);
|
|
22
|
+
if (k === "class") {
|
|
23
|
+
for (const c of value.split(" ")) {
|
|
24
|
+
if (!$el.classList.contains(c)) {
|
|
25
|
+
$el.classList.add(c);
|
|
26
|
+
head._removeQueuedSideEffect(`${attrSdeKey}:${c}`);
|
|
27
|
+
entry._sde[`${attrSdeKey}:${c}`] = () => $el.classList.remove(c);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if ($el.getAttribute(k) !== value) {
|
|
33
|
+
$el.setAttribute(k, value);
|
|
34
|
+
if (!k.startsWith("data-h-"))
|
|
35
|
+
entry._sde[attrSdeKey] = () => $el.removeAttribute(k);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function renderDOMHead(head, options = {}) {
|
|
41
|
+
const dom = options.document || window.document;
|
|
42
|
+
const tags = await head.resolveTags();
|
|
43
|
+
await head.hooks.callHook("dom:beforeRender", { head, tags, document: dom });
|
|
44
|
+
for (const tag of tags) {
|
|
45
|
+
const renderCtx = { tag, document: dom, head };
|
|
46
|
+
await head.hooks.callHook("dom:renderTag", renderCtx);
|
|
47
|
+
const entry = head.headEntries().find((e) => e._i === Number(tag._e));
|
|
48
|
+
if (tag.tag === "title" && tag.children) {
|
|
49
|
+
dom.title = tag.children;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (tag.tag === "htmlAttrs" || tag.tag === "bodyAttrs") {
|
|
53
|
+
setAttributesWithSideEffects(head, dom[tag.tag === "htmlAttrs" ? "documentElement" : "body"], entry, tag);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const sdeKey = `${tag._s || tag._p}:el`;
|
|
57
|
+
const $newEl = createElement(tag, dom);
|
|
58
|
+
let $previousEl = null;
|
|
59
|
+
for (const $el of dom[tag.tagPosition?.startsWith("body") ? "body" : "head"].children) {
|
|
60
|
+
if ($el.hasAttribute(`${tag._s}`)) {
|
|
61
|
+
$previousEl = $el;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if ($previousEl) {
|
|
66
|
+
head._removeQueuedSideEffect(sdeKey);
|
|
67
|
+
if ($newEl.isEqualNode($previousEl))
|
|
68
|
+
continue;
|
|
69
|
+
if (Object.keys(tag.props).length === 0) {
|
|
70
|
+
$previousEl.remove();
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
setAttributesWithSideEffects(head, $previousEl, entry, tag);
|
|
74
|
+
if (TagsWithInnerContent.includes(tag.tag))
|
|
75
|
+
$previousEl.innerHTML = tag.children || "";
|
|
76
|
+
entry._sde[sdeKey] = () => $previousEl?.remove();
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
switch (tag.tagPosition) {
|
|
80
|
+
case "bodyClose":
|
|
81
|
+
dom.body.appendChild($newEl);
|
|
82
|
+
break;
|
|
83
|
+
case "bodyOpen":
|
|
84
|
+
dom.body.insertBefore($newEl, dom.body.firstChild);
|
|
85
|
+
break;
|
|
86
|
+
case "head":
|
|
87
|
+
default:
|
|
88
|
+
dom.head.appendChild($newEl);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
entry._sde[sdeKey] = () => $newEl?.remove();
|
|
92
|
+
}
|
|
93
|
+
head._flushQueuedSideEffects();
|
|
94
|
+
}
|
|
95
|
+
let domUpdatePromise = null;
|
|
96
|
+
async function debouncedRenderDOMHead(delayedFn, head, options = {}) {
|
|
97
|
+
function doDomUpdate() {
|
|
98
|
+
domUpdatePromise = null;
|
|
99
|
+
return renderDOMHead(head, options);
|
|
100
|
+
}
|
|
101
|
+
return domUpdatePromise = domUpdatePromise || new Promise((resolve) => delayedFn(() => resolve(doDomUpdate())));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export { debouncedRenderDOMHead, domUpdatePromise, renderDOMHead };
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@unhead/dom",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"packageManager": "pnpm@7.14.0",
|
|
6
|
+
"author": "Harlan Wilton <harlan@harlanzw.com>",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"funding": "https://github.com/sponsors/harlan-zw",
|
|
9
|
+
"homepage": "https://github.com/harlan-zw/unhead#readme",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/harlan-zw/unhead.git",
|
|
13
|
+
"directory": "packages/dom"
|
|
14
|
+
},
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/harlan-zw/unhead/issues"
|
|
17
|
+
},
|
|
18
|
+
"sideEffects": false,
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.mjs"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"module": "dist/index.mjs",
|
|
26
|
+
"types": "dist/index.d.ts",
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@unhead/schema": "0.1.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"zhead": "1.0.0-beta.4"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "unbuild .",
|
|
38
|
+
"stub": "unbuild . --stub",
|
|
39
|
+
"export:sizes": "npx export-size . -r"
|
|
40
|
+
}
|
|
41
|
+
}
|