@unhead/dom 1.2.1 → 1.3.0-beta.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.cjs CHANGED
@@ -2,204 +2,128 @@
2
2
 
3
3
  const shared = require('@unhead/shared');
4
4
 
5
- function setAttrs(ctx, newEntry = false, markSideEffect) {
6
- const { tag, $el } = ctx;
7
- if (!$el)
8
- return;
9
- Object.entries(tag.props).forEach(([k, value]) => {
10
- value = String(value);
11
- const attrSdeKey = `attr:${k}`;
12
- if (k === "class") {
13
- if (!value)
14
- return;
15
- for (const c of value.split(" ")) {
16
- const classSdeKey = `${attrSdeKey}:${c}`;
17
- if (markSideEffect)
18
- markSideEffect(ctx, classSdeKey, () => $el.classList.remove(c));
19
- if (!$el.classList.contains(c))
20
- $el.classList.add(c);
21
- }
22
- return;
23
- }
24
- if (markSideEffect && !k.startsWith("data-h-"))
25
- markSideEffect(ctx, attrSdeKey, () => $el.removeAttribute(k));
26
- if (newEntry || $el.getAttribute(k) !== value)
27
- $el.setAttribute(k, value);
28
- });
29
- if (shared.TagsWithInnerContent.includes(tag.tag)) {
30
- if (tag.textContent && tag.textContent !== $el.textContent)
31
- $el.textContent = tag.textContent;
32
- else if (tag.innerHTML && tag.innerHTML !== $el.innerHTML)
33
- $el.innerHTML = tag.innerHTML;
34
- }
5
+ function elementToTag($el) {
6
+ const tag = {
7
+ tag: $el.tagName.toLowerCase(),
8
+ props: $el.getAttributeNames().reduce((props, name) => ({ ...props, [name]: $el.getAttribute(name) }), {}),
9
+ innerHTML: $el.innerHTML
10
+ };
11
+ tag._d = shared.tagDedupeKey(tag);
12
+ return tag;
35
13
  }
36
-
37
- let prevHash = false;
38
14
  async function renderDOMHead(head, options = {}) {
39
- const beforeRenderCtx = { shouldRender: true };
15
+ const dom = options.document || head.resolvedOptions.document;
16
+ if (!dom)
17
+ return;
18
+ const tags = (await head.resolveTags()).map((tag) => ({
19
+ tag,
20
+ id: shared.HasElementTags.includes(tag.tag) ? shared.hashTag(tag) : tag.tag,
21
+ shouldRender: true
22
+ }));
23
+ const beforeRenderCtx = { shouldRender: true, tags };
40
24
  await head.hooks.callHook("dom:beforeRender", beforeRenderCtx);
41
25
  if (!beforeRenderCtx.shouldRender)
42
26
  return;
43
- const dom = options.document || head.resolvedOptions.document || window.document;
44
- const tagContexts = (await head.resolveTags()).map(setupTagRenderCtx);
45
- if (head.resolvedOptions.experimentalHashHydration) {
46
- prevHash = prevHash || head._hash || false;
47
- if (prevHash) {
48
- const hash = shared.computeHashes(tagContexts.map((ctx) => ctx.tag._h));
49
- if (prevHash === hash)
50
- return;
51
- prevHash = hash;
27
+ let state = head._dom;
28
+ if (!state) {
29
+ state = {
30
+ elMap: { htmlAttrs: dom.documentElement, bodyAttrs: dom.body }
31
+ };
32
+ for (const key of ["body", "head"]) {
33
+ const children = dom?.[key]?.children;
34
+ for (const c of [...children].filter((c2) => shared.HasElementTags.includes(c2.tagName.toLowerCase())))
35
+ state.elMap[c.getAttribute("data-hid") || shared.hashTag(elementToTag(c))] = c;
52
36
  }
53
37
  }
54
- const staleSideEffects = head._popSideEffectQueue();
55
- head.headEntries().map((entry) => entry._sde).forEach((sde) => {
56
- Object.entries(sde).forEach(([key, fn]) => {
57
- staleSideEffects[key] = fn;
58
- });
59
- });
60
- const markSideEffect = (ctx, key, fn) => {
61
- key = `${ctx.renderId}:${key}`;
62
- if (ctx.entry)
63
- ctx.entry._sde[key] = fn;
64
- delete staleSideEffects[key];
65
- };
66
- function setupTagRenderCtx(tag) {
67
- const entry = head.headEntries().find((e) => e._i === tag._e);
68
- const renderCtx = {
69
- renderId: tag._d || shared.hashTag(tag),
70
- $el: null,
71
- shouldRender: true,
72
- tag,
73
- entry,
74
- markSideEffect: (key, fn) => markSideEffect(renderCtx, key, fn)
75
- };
76
- return renderCtx;
38
+ state.pendingSideEffects = { ...state.sideEffects || {} };
39
+ state.sideEffects = {};
40
+ function track(id, scope, fn) {
41
+ const k = `${id}:${scope}`;
42
+ state.sideEffects[k] = fn;
43
+ delete state.pendingSideEffects[k];
77
44
  }
78
- const renders = [];
79
- const pendingRenders = {
80
- body: [],
81
- head: []
82
- };
83
- const markEl = (ctx) => {
84
- head._elMap[ctx.renderId] = ctx.$el;
85
- renders.push(ctx);
86
- markSideEffect(ctx, "el", () => {
87
- ctx.$el?.remove();
88
- delete head._elMap[ctx.renderId];
89
- });
90
- };
91
- for (const ctx of tagContexts) {
92
- await head.hooks.callHook("dom:beforeRenderTag", ctx);
93
- if (!ctx.shouldRender)
94
- continue;
95
- const { tag } = ctx;
96
- if (tag.tag === "title") {
97
- dom.title = tag.textContent || "";
98
- renders.push(ctx);
99
- continue;
45
+ function trackCtx({ id, $el, tag }) {
46
+ const isAttrTag = tag.tag.endsWith("Attrs");
47
+ state.elMap[id] = $el;
48
+ if (!isAttrTag) {
49
+ ["textContent", "innerHTML"].forEach((k) => {
50
+ tag[k] && tag[k] !== $el[k] && ($el[k] = tag[k]);
51
+ });
52
+ track(id, "el", () => {
53
+ state.elMap[id].remove();
54
+ delete state.elMap[id];
55
+ });
100
56
  }
101
- if (tag.tag === "htmlAttrs" || tag.tag === "bodyAttrs") {
102
- ctx.$el = dom[tag.tag === "htmlAttrs" ? "documentElement" : "body"];
103
- setAttrs(ctx, false, markSideEffect);
104
- renders.push(ctx);
105
- continue;
106
- }
107
- ctx.$el = head._elMap[ctx.renderId];
108
- if (!ctx.$el && tag.key)
109
- ctx.$el = dom.querySelector(`${tag.tagPosition?.startsWith("body") ? "body" : "head"} > ${tag.tag}[data-h-${tag._h}]`);
110
- if (ctx.$el) {
111
- if (ctx.tag._d)
112
- setAttrs(ctx);
113
- markEl(ctx);
114
- continue;
115
- }
116
- pendingRenders[tag.tagPosition?.startsWith("body") ? "body" : "head"].push(ctx);
57
+ Object.entries(tag.props).forEach(([k, value]) => {
58
+ value = String(value);
59
+ const ck = `attr:${k}`;
60
+ if (k === "class") {
61
+ for (const c of (value || "").split(" ").filter(Boolean)) {
62
+ isAttrTag && track(id, `${ck}:${c}`, () => $el.classList.remove(c));
63
+ !$el.classList.contains(c) && $el.classList.add(c);
64
+ }
65
+ } else {
66
+ $el.getAttribute(k) !== value && $el.setAttribute(k, value);
67
+ isAttrTag && track(id, ck, () => $el.removeAttribute(k));
68
+ }
69
+ });
117
70
  }
118
- const fragments = {
71
+ const pending = [];
72
+ const frag = {
119
73
  bodyClose: void 0,
120
74
  bodyOpen: void 0,
121
75
  head: void 0
122
76
  };
123
- Object.entries(pendingRenders).forEach(([pos, queue]) => {
124
- if (!queue.length)
125
- return;
126
- const children = dom?.[pos]?.children;
127
- if (!children)
128
- return;
129
- for (const $el of [...children].reverse()) {
130
- const elTag = $el.tagName.toLowerCase();
131
- if (!shared.HasElementTags.includes(elTag))
132
- continue;
133
- const props = $el.getAttributeNames().reduce((props2, name) => ({ ...props2, [name]: $el.getAttribute(name) }), {});
134
- const tmpTag = { tag: elTag, props };
135
- if ($el.innerHTML)
136
- tmpTag.innerHTML = $el.innerHTML;
137
- const tmpRenderId = shared.hashTag(tmpTag);
138
- let matchIdx = queue.findIndex((ctx) => ctx?.renderId === tmpRenderId);
139
- if (matchIdx === -1) {
140
- const tmpDedupeKey = shared.tagDedupeKey(tmpTag);
141
- matchIdx = queue.findIndex((ctx) => ctx?.tag._d && ctx.tag._d === tmpDedupeKey);
142
- }
143
- if (matchIdx !== -1) {
144
- const ctx = queue[matchIdx];
145
- ctx.$el = $el;
146
- setAttrs(ctx);
147
- markEl(ctx);
148
- delete queue[matchIdx];
149
- }
77
+ for (const ctx of tags) {
78
+ const { tag, shouldRender, id } = ctx;
79
+ if (!shouldRender)
80
+ continue;
81
+ if (tag.tag === "title") {
82
+ dom.title = tag.textContent;
83
+ continue;
150
84
  }
151
- queue.forEach((ctx) => {
152
- const pos2 = ctx.tag.tagPosition || "head";
153
- fragments[pos2] = fragments[pos2] || dom.createDocumentFragment();
154
- if (!ctx.$el) {
155
- ctx.$el = dom.createElement(ctx.tag.tag);
156
- setAttrs(ctx, true);
157
- }
158
- fragments[pos2].appendChild(ctx.$el);
159
- markEl(ctx);
160
- });
161
- });
162
- if (fragments.head)
163
- dom.head.appendChild(fragments.head);
164
- if (fragments.bodyOpen)
165
- dom.body.insertBefore(fragments.bodyOpen, dom.body.firstChild);
166
- if (fragments.bodyClose)
167
- dom.body.appendChild(fragments.bodyClose);
168
- for (const ctx of renders)
169
- await head.hooks.callHook("dom:renderTag", ctx);
170
- Object.values(staleSideEffects).forEach((fn) => fn());
85
+ ctx.$el = ctx.$el || state.elMap[id];
86
+ if (ctx.$el)
87
+ trackCtx(ctx);
88
+ else
89
+ shared.HasElementTags.includes(tag.tag) && pending.push(ctx);
90
+ }
91
+ for (const ctx of pending) {
92
+ const pos = ctx.tag.tagPosition || "head";
93
+ ctx.$el = dom.createElement(ctx.tag.tag);
94
+ trackCtx(ctx);
95
+ frag[pos] = frag[pos] || dom.createDocumentFragment();
96
+ frag[pos].appendChild(ctx.$el);
97
+ }
98
+ for (const ctx of tags)
99
+ await head.hooks.callHook("dom:renderTag", ctx, dom, track);
100
+ frag.head && dom.head.appendChild(frag.head);
101
+ frag.bodyOpen && dom.body.insertBefore(frag.bodyOpen, dom.body.firstChild);
102
+ frag.bodyClose && dom.body.appendChild(frag.bodyClose);
103
+ Object.values(state.pendingSideEffects).forEach((fn) => fn());
104
+ head._dom = state;
105
+ await head.hooks.callHook("dom:rendered", { renders: tags });
171
106
  }
172
- exports.domUpdatePromise = null;
107
+
173
108
  async function debouncedRenderDOMHead(head, options = {}) {
174
- function doDomUpdate() {
175
- exports.domUpdatePromise = null;
176
- return renderDOMHead(head, options);
177
- }
178
- const delayFn = options.delayFn || ((fn) => setTimeout(fn, 10));
179
- return exports.domUpdatePromise = exports.domUpdatePromise || new Promise((resolve) => delayFn(() => resolve(doDomUpdate())));
109
+ const fn = options.delayFn || ((fn2) => setTimeout(fn2, 10));
110
+ return head._domUpdatePromise = head._domUpdatePromise || new Promise((resolve) => fn(async () => {
111
+ await renderDOMHead(head, options);
112
+ delete head._domUpdatePromise;
113
+ resolve();
114
+ }));
180
115
  }
181
116
 
182
117
  function PatchDomOnEntryUpdatesPlugin(options) {
183
118
  return shared.defineHeadPlugin({
184
119
  hooks: {
185
120
  "entries:updated": function(head) {
186
- if (typeof options?.document === "undefined" && typeof window === "undefined")
187
- return;
188
- let delayFn = options?.delayFn;
189
- if (!delayFn && typeof requestAnimationFrame !== "undefined")
190
- delayFn = requestAnimationFrame;
191
- debouncedRenderDOMHead(head, { document: options?.document || window.document, delayFn });
121
+ debouncedRenderDOMHead(head, options);
192
122
  }
193
123
  }
194
124
  });
195
125
  }
196
126
 
197
- function maybeGetSSRHash(document) {
198
- return document?.head.querySelector('meta[name="unhead:ssr"]')?.getAttribute("content") || false;
199
- }
200
-
201
127
  exports.PatchDomOnEntryUpdatesPlugin = PatchDomOnEntryUpdatesPlugin;
202
128
  exports.debouncedRenderDOMHead = debouncedRenderDOMHead;
203
- exports.maybeGetSSRHash = maybeGetSSRHash;
204
129
  exports.renderDOMHead = renderDOMHead;
205
- exports.setAttrs = setAttrs;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _unhead_schema from '@unhead/schema';
2
- import { Unhead, DomRenderTagContext } from '@unhead/schema';
2
+ import { Unhead } from '@unhead/schema';
3
3
 
4
4
  interface RenderDomHeadOptions {
5
5
  /**
@@ -7,35 +7,25 @@ interface RenderDomHeadOptions {
7
7
  */
8
8
  document?: Document;
9
9
  }
10
- interface DebouncedRenderDomHeadOptions extends RenderDomHeadOptions {
11
- /**
12
- * Specify a custom delay function for delaying the render.
13
- */
14
- delayFn?: (fn: () => void) => void;
15
- }
16
10
  /**
17
11
  * Render the head tags to the DOM.
18
12
  */
19
13
  declare function renderDOMHead<T extends Unhead<any>>(head: T, options?: RenderDomHeadOptions): Promise<void>;
20
- /**
21
- * Global instance of the dom update promise. Used for debounding head updates.
22
- */
23
- declare let domUpdatePromise: Promise<void> | null;
24
- /**
25
- * Queue a debounced update of the DOM head.
26
- */
27
- declare function debouncedRenderDOMHead<T extends Unhead<any>>(head: T, options?: DebouncedRenderDomHeadOptions): Promise<void>;
28
14
 
29
15
  interface TriggerDomPatchingOnUpdatesPluginOptions extends RenderDomHeadOptions {
30
16
  delayFn?: (fn: () => void) => void;
31
17
  }
32
18
  declare function PatchDomOnEntryUpdatesPlugin(options?: TriggerDomPatchingOnUpdatesPluginOptions): _unhead_schema.HeadPlugin;
33
19
 
20
+ interface DebouncedRenderDomHeadOptions extends RenderDomHeadOptions {
21
+ /**
22
+ * Specify a custom delay function for delaying the render.
23
+ */
24
+ delayFn?: (fn: () => void) => void;
25
+ }
34
26
  /**
35
- * Set attributes on a DOM element, while adding entry side effects.
27
+ * Queue a debounced update of the DOM head.
36
28
  */
37
- declare function setAttrs(ctx: DomRenderTagContext, newEntry?: boolean, markSideEffect?: (ctx: DomRenderTagContext, k: string, fn: () => void) => void): void;
38
-
39
- declare function maybeGetSSRHash(document: Document): string | false;
29
+ declare function debouncedRenderDOMHead<T extends Unhead<any>>(head: T, options?: DebouncedRenderDomHeadOptions): Promise<void>;
40
30
 
41
- export { DebouncedRenderDomHeadOptions, PatchDomOnEntryUpdatesPlugin, RenderDomHeadOptions, TriggerDomPatchingOnUpdatesPluginOptions, debouncedRenderDOMHead, domUpdatePromise, maybeGetSSRHash, renderDOMHead, setAttrs };
31
+ export { DebouncedRenderDomHeadOptions, PatchDomOnEntryUpdatesPlugin, RenderDomHeadOptions, TriggerDomPatchingOnUpdatesPluginOptions, debouncedRenderDOMHead, renderDOMHead };
package/dist/index.mjs CHANGED
@@ -1,199 +1,125 @@
1
- import { TagsWithInnerContent, computeHashes, hashTag, HasElementTags, tagDedupeKey, defineHeadPlugin } from '@unhead/shared';
1
+ import { HasElementTags, hashTag, tagDedupeKey, defineHeadPlugin } from '@unhead/shared';
2
2
 
3
- function setAttrs(ctx, newEntry = false, markSideEffect) {
4
- const { tag, $el } = ctx;
5
- if (!$el)
6
- return;
7
- Object.entries(tag.props).forEach(([k, value]) => {
8
- value = String(value);
9
- const attrSdeKey = `attr:${k}`;
10
- if (k === "class") {
11
- if (!value)
12
- return;
13
- for (const c of value.split(" ")) {
14
- const classSdeKey = `${attrSdeKey}:${c}`;
15
- if (markSideEffect)
16
- markSideEffect(ctx, classSdeKey, () => $el.classList.remove(c));
17
- if (!$el.classList.contains(c))
18
- $el.classList.add(c);
19
- }
20
- return;
21
- }
22
- if (markSideEffect && !k.startsWith("data-h-"))
23
- markSideEffect(ctx, attrSdeKey, () => $el.removeAttribute(k));
24
- if (newEntry || $el.getAttribute(k) !== value)
25
- $el.setAttribute(k, value);
26
- });
27
- if (TagsWithInnerContent.includes(tag.tag)) {
28
- if (tag.textContent && tag.textContent !== $el.textContent)
29
- $el.textContent = tag.textContent;
30
- else if (tag.innerHTML && tag.innerHTML !== $el.innerHTML)
31
- $el.innerHTML = tag.innerHTML;
32
- }
3
+ function elementToTag($el) {
4
+ const tag = {
5
+ tag: $el.tagName.toLowerCase(),
6
+ props: $el.getAttributeNames().reduce((props, name) => ({ ...props, [name]: $el.getAttribute(name) }), {}),
7
+ innerHTML: $el.innerHTML
8
+ };
9
+ tag._d = tagDedupeKey(tag);
10
+ return tag;
33
11
  }
34
-
35
- let prevHash = false;
36
12
  async function renderDOMHead(head, options = {}) {
37
- const beforeRenderCtx = { shouldRender: true };
13
+ const dom = options.document || head.resolvedOptions.document;
14
+ if (!dom)
15
+ return;
16
+ const tags = (await head.resolveTags()).map((tag) => ({
17
+ tag,
18
+ id: HasElementTags.includes(tag.tag) ? hashTag(tag) : tag.tag,
19
+ shouldRender: true
20
+ }));
21
+ const beforeRenderCtx = { shouldRender: true, tags };
38
22
  await head.hooks.callHook("dom:beforeRender", beforeRenderCtx);
39
23
  if (!beforeRenderCtx.shouldRender)
40
24
  return;
41
- const dom = options.document || head.resolvedOptions.document || window.document;
42
- const tagContexts = (await head.resolveTags()).map(setupTagRenderCtx);
43
- if (head.resolvedOptions.experimentalHashHydration) {
44
- prevHash = prevHash || head._hash || false;
45
- if (prevHash) {
46
- const hash = computeHashes(tagContexts.map((ctx) => ctx.tag._h));
47
- if (prevHash === hash)
48
- return;
49
- prevHash = hash;
25
+ let state = head._dom;
26
+ if (!state) {
27
+ state = {
28
+ elMap: { htmlAttrs: dom.documentElement, bodyAttrs: dom.body }
29
+ };
30
+ for (const key of ["body", "head"]) {
31
+ const children = dom?.[key]?.children;
32
+ for (const c of [...children].filter((c2) => HasElementTags.includes(c2.tagName.toLowerCase())))
33
+ state.elMap[c.getAttribute("data-hid") || hashTag(elementToTag(c))] = c;
50
34
  }
51
35
  }
52
- const staleSideEffects = head._popSideEffectQueue();
53
- head.headEntries().map((entry) => entry._sde).forEach((sde) => {
54
- Object.entries(sde).forEach(([key, fn]) => {
55
- staleSideEffects[key] = fn;
56
- });
57
- });
58
- const markSideEffect = (ctx, key, fn) => {
59
- key = `${ctx.renderId}:${key}`;
60
- if (ctx.entry)
61
- ctx.entry._sde[key] = fn;
62
- delete staleSideEffects[key];
63
- };
64
- function setupTagRenderCtx(tag) {
65
- const entry = head.headEntries().find((e) => e._i === tag._e);
66
- const renderCtx = {
67
- renderId: tag._d || hashTag(tag),
68
- $el: null,
69
- shouldRender: true,
70
- tag,
71
- entry,
72
- markSideEffect: (key, fn) => markSideEffect(renderCtx, key, fn)
73
- };
74
- return renderCtx;
36
+ state.pendingSideEffects = { ...state.sideEffects || {} };
37
+ state.sideEffects = {};
38
+ function track(id, scope, fn) {
39
+ const k = `${id}:${scope}`;
40
+ state.sideEffects[k] = fn;
41
+ delete state.pendingSideEffects[k];
75
42
  }
76
- const renders = [];
77
- const pendingRenders = {
78
- body: [],
79
- head: []
80
- };
81
- const markEl = (ctx) => {
82
- head._elMap[ctx.renderId] = ctx.$el;
83
- renders.push(ctx);
84
- markSideEffect(ctx, "el", () => {
85
- ctx.$el?.remove();
86
- delete head._elMap[ctx.renderId];
87
- });
88
- };
89
- for (const ctx of tagContexts) {
90
- await head.hooks.callHook("dom:beforeRenderTag", ctx);
91
- if (!ctx.shouldRender)
92
- continue;
93
- const { tag } = ctx;
94
- if (tag.tag === "title") {
95
- dom.title = tag.textContent || "";
96
- renders.push(ctx);
97
- continue;
43
+ function trackCtx({ id, $el, tag }) {
44
+ const isAttrTag = tag.tag.endsWith("Attrs");
45
+ state.elMap[id] = $el;
46
+ if (!isAttrTag) {
47
+ ["textContent", "innerHTML"].forEach((k) => {
48
+ tag[k] && tag[k] !== $el[k] && ($el[k] = tag[k]);
49
+ });
50
+ track(id, "el", () => {
51
+ state.elMap[id].remove();
52
+ delete state.elMap[id];
53
+ });
98
54
  }
99
- if (tag.tag === "htmlAttrs" || tag.tag === "bodyAttrs") {
100
- ctx.$el = dom[tag.tag === "htmlAttrs" ? "documentElement" : "body"];
101
- setAttrs(ctx, false, markSideEffect);
102
- renders.push(ctx);
103
- continue;
104
- }
105
- ctx.$el = head._elMap[ctx.renderId];
106
- if (!ctx.$el && tag.key)
107
- ctx.$el = dom.querySelector(`${tag.tagPosition?.startsWith("body") ? "body" : "head"} > ${tag.tag}[data-h-${tag._h}]`);
108
- if (ctx.$el) {
109
- if (ctx.tag._d)
110
- setAttrs(ctx);
111
- markEl(ctx);
112
- continue;
113
- }
114
- pendingRenders[tag.tagPosition?.startsWith("body") ? "body" : "head"].push(ctx);
55
+ Object.entries(tag.props).forEach(([k, value]) => {
56
+ value = String(value);
57
+ const ck = `attr:${k}`;
58
+ if (k === "class") {
59
+ for (const c of (value || "").split(" ").filter(Boolean)) {
60
+ isAttrTag && track(id, `${ck}:${c}`, () => $el.classList.remove(c));
61
+ !$el.classList.contains(c) && $el.classList.add(c);
62
+ }
63
+ } else {
64
+ $el.getAttribute(k) !== value && $el.setAttribute(k, value);
65
+ isAttrTag && track(id, ck, () => $el.removeAttribute(k));
66
+ }
67
+ });
115
68
  }
116
- const fragments = {
69
+ const pending = [];
70
+ const frag = {
117
71
  bodyClose: void 0,
118
72
  bodyOpen: void 0,
119
73
  head: void 0
120
74
  };
121
- Object.entries(pendingRenders).forEach(([pos, queue]) => {
122
- if (!queue.length)
123
- return;
124
- const children = dom?.[pos]?.children;
125
- if (!children)
126
- return;
127
- for (const $el of [...children].reverse()) {
128
- const elTag = $el.tagName.toLowerCase();
129
- if (!HasElementTags.includes(elTag))
130
- continue;
131
- const props = $el.getAttributeNames().reduce((props2, name) => ({ ...props2, [name]: $el.getAttribute(name) }), {});
132
- const tmpTag = { tag: elTag, props };
133
- if ($el.innerHTML)
134
- tmpTag.innerHTML = $el.innerHTML;
135
- const tmpRenderId = hashTag(tmpTag);
136
- let matchIdx = queue.findIndex((ctx) => ctx?.renderId === tmpRenderId);
137
- if (matchIdx === -1) {
138
- const tmpDedupeKey = tagDedupeKey(tmpTag);
139
- matchIdx = queue.findIndex((ctx) => ctx?.tag._d && ctx.tag._d === tmpDedupeKey);
140
- }
141
- if (matchIdx !== -1) {
142
- const ctx = queue[matchIdx];
143
- ctx.$el = $el;
144
- setAttrs(ctx);
145
- markEl(ctx);
146
- delete queue[matchIdx];
147
- }
75
+ for (const ctx of tags) {
76
+ const { tag, shouldRender, id } = ctx;
77
+ if (!shouldRender)
78
+ continue;
79
+ if (tag.tag === "title") {
80
+ dom.title = tag.textContent;
81
+ continue;
148
82
  }
149
- queue.forEach((ctx) => {
150
- const pos2 = ctx.tag.tagPosition || "head";
151
- fragments[pos2] = fragments[pos2] || dom.createDocumentFragment();
152
- if (!ctx.$el) {
153
- ctx.$el = dom.createElement(ctx.tag.tag);
154
- setAttrs(ctx, true);
155
- }
156
- fragments[pos2].appendChild(ctx.$el);
157
- markEl(ctx);
158
- });
159
- });
160
- if (fragments.head)
161
- dom.head.appendChild(fragments.head);
162
- if (fragments.bodyOpen)
163
- dom.body.insertBefore(fragments.bodyOpen, dom.body.firstChild);
164
- if (fragments.bodyClose)
165
- dom.body.appendChild(fragments.bodyClose);
166
- for (const ctx of renders)
167
- await head.hooks.callHook("dom:renderTag", ctx);
168
- Object.values(staleSideEffects).forEach((fn) => fn());
83
+ ctx.$el = ctx.$el || state.elMap[id];
84
+ if (ctx.$el)
85
+ trackCtx(ctx);
86
+ else
87
+ HasElementTags.includes(tag.tag) && pending.push(ctx);
88
+ }
89
+ for (const ctx of pending) {
90
+ const pos = ctx.tag.tagPosition || "head";
91
+ ctx.$el = dom.createElement(ctx.tag.tag);
92
+ trackCtx(ctx);
93
+ frag[pos] = frag[pos] || dom.createDocumentFragment();
94
+ frag[pos].appendChild(ctx.$el);
95
+ }
96
+ for (const ctx of tags)
97
+ await head.hooks.callHook("dom:renderTag", ctx, dom, track);
98
+ frag.head && dom.head.appendChild(frag.head);
99
+ frag.bodyOpen && dom.body.insertBefore(frag.bodyOpen, dom.body.firstChild);
100
+ frag.bodyClose && dom.body.appendChild(frag.bodyClose);
101
+ Object.values(state.pendingSideEffects).forEach((fn) => fn());
102
+ head._dom = state;
103
+ await head.hooks.callHook("dom:rendered", { renders: tags });
169
104
  }
170
- let domUpdatePromise = null;
105
+
171
106
  async function debouncedRenderDOMHead(head, options = {}) {
172
- function doDomUpdate() {
173
- domUpdatePromise = null;
174
- return renderDOMHead(head, options);
175
- }
176
- const delayFn = options.delayFn || ((fn) => setTimeout(fn, 10));
177
- return domUpdatePromise = domUpdatePromise || new Promise((resolve) => delayFn(() => resolve(doDomUpdate())));
107
+ const fn = options.delayFn || ((fn2) => setTimeout(fn2, 10));
108
+ return head._domUpdatePromise = head._domUpdatePromise || new Promise((resolve) => fn(async () => {
109
+ await renderDOMHead(head, options);
110
+ delete head._domUpdatePromise;
111
+ resolve();
112
+ }));
178
113
  }
179
114
 
180
115
  function PatchDomOnEntryUpdatesPlugin(options) {
181
116
  return defineHeadPlugin({
182
117
  hooks: {
183
118
  "entries:updated": function(head) {
184
- if (typeof options?.document === "undefined" && typeof window === "undefined")
185
- return;
186
- let delayFn = options?.delayFn;
187
- if (!delayFn && typeof requestAnimationFrame !== "undefined")
188
- delayFn = requestAnimationFrame;
189
- debouncedRenderDOMHead(head, { document: options?.document || window.document, delayFn });
119
+ debouncedRenderDOMHead(head, options);
190
120
  }
191
121
  }
192
122
  });
193
123
  }
194
124
 
195
- function maybeGetSSRHash(document) {
196
- return document?.head.querySelector('meta[name="unhead:ssr"]')?.getAttribute("content") || false;
197
- }
198
-
199
- export { PatchDomOnEntryUpdatesPlugin, debouncedRenderDOMHead, domUpdatePromise, maybeGetSSRHash, renderDOMHead, setAttrs };
125
+ export { PatchDomOnEntryUpdatesPlugin, debouncedRenderDOMHead, renderDOMHead };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@unhead/dom",
3
3
  "type": "module",
4
- "version": "1.2.1",
4
+ "version": "1.3.0-beta.0",
5
5
  "author": "Harlan Wilton <harlan@harlanzw.com>",
6
6
  "license": "MIT",
7
7
  "funding": "https://github.com/sponsors/harlan-zw",
@@ -29,8 +29,8 @@
29
29
  "dist"
30
30
  ],
31
31
  "dependencies": {
32
- "@unhead/schema": "1.2.1",
33
- "@unhead/shared": "1.2.1"
32
+ "@unhead/schema": "1.3.0-beta.0",
33
+ "@unhead/shared": "1.3.0-beta.0"
34
34
  },
35
35
  "scripts": {
36
36
  "build": "unbuild .",