-
-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathHeadManager.mjs
More file actions
104 lines (86 loc) · 2.99 KB
/
HeadManager.mjs
File metadata and controls
104 lines (86 loc) · 2.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// @ts-check
/** @import { ReactNode } from "react" */
import { createElement as h, Fragment } from "react";
/** Document head tag manager {@linkcode Fragment}. */
export default class HeadManager extends EventTarget {
constructor() {
super();
/** @type {Map<ReactNode, { key: string, priority: number }>} */
this.managed = new Map();
}
/**
* Gets the managed document head tag content.
*
* If multiple entries have the same head tag fragment key, higher priority or
* later added ones override.
*
* The final head tag fragments are ordered by key, ensuring:
*
* - The project author can control the order.
* - Adding or removing managed document head tags causes minimal React
* rendering DOM mutations that can cause FOUC.
*/
getHeadContent() {
/** @type {Map<string, { priority: number, content: ReactNode }>} */
const deduped = new Map();
for (const [content, { key, priority }] of [...this.managed].reverse()) {
const existing = deduped.get(key);
if (!existing || existing.priority < priority) {
deduped.set(key, { priority, content });
}
}
const sorted = new Map([...deduped].sort(([a], [b]) => a.localeCompare(b)));
const content = [];
for (const [key, value] of sorted) {
content.push(h(Fragment, { key }, value.content));
}
return content;
}
/**
* Adds document head tags.
* @param {string} key Head tag fragment key.
* @param {ReactNode} content Memoized React content containing head tags.
* @param {number} [priority=0] Priority. Higher priority managed head tags
* override lower priority ones with the same head tag fragment key.
*/
add(key, content, priority = 0) {
if (typeof key !== "string") {
throw new TypeError("Argument 1 `key` must be a string.");
}
if (arguments.length < 2) {
throw new TypeError("Argument 2 `content` must be specified.");
}
if (typeof priority !== "number") {
throw new TypeError("Argument 3 `priority` must be a number.");
}
const preexisting = this.managed.get(content);
if (!preexisting) {
this.managed.set(content, { key, priority });
this.dispatchEvent(new CustomEvent("update"));
} else {
if (key !== preexisting.key) {
throw new TypeError(
`Argument 2 \`content\` already added with a different \`key\` of \`${preexisting.key}\`.`,
);
}
if (priority !== preexisting.priority) {
throw new TypeError(
`Argument 2 \`content\` already added with a different \`priority\` of \`${preexisting.priority}\`.`,
);
}
// Do nothing as this exact combination of arguments has already been
// added.
}
}
/**
* Removes document head tags.
* @param {ReactNode} content Memoized React content containing head tags to
* remove.
*/
remove(content) {
if (this.managed.has(content)) {
this.managed.delete(content);
this.dispatchEvent(new CustomEvent("update"));
}
}
}