-
-
Notifications
You must be signed in to change notification settings - Fork 71
Expand file tree
/
Copy pathuse-head-changes.ts
More file actions
110 lines (102 loc) · 2.9 KB
/
use-head-changes.ts
File metadata and controls
110 lines (102 loc) · 2.9 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
105
106
107
108
109
110
import { onCleanup, onMount } from 'solid-js'
type HeadChange =
| { kind: 'added'; node: Node }
| { kind: 'removed'; node: Node }
| {
kind: 'attr'
target: Element
name: string | null
oldValue: string | null
}
| { kind: 'title'; title: string }
type UseHeadChangesOptions = {
/**
* Observe attribute changes on elements inside <head>
* Default: true
*/
attributes?: boolean
/**
* Observe added/removed nodes in <head>
* Default: true
*/
childList?: boolean
/**
* Observe descendants of <head>
* Default: true
*/
subtree?: boolean
/**
* Also observe <title> changes explicitly
* Default: true
*/
observeTitle?: boolean
}
export function useHeadChanges(
onChange: (change: HeadChange, raw?: MutationRecord) => void,
opts: UseHeadChangesOptions = {},
) {
const {
attributes = true,
childList = true,
subtree = true,
observeTitle = true,
} = opts
onMount(() => {
const headObserver = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.type === 'childList') {
m.addedNodes.forEach((node) => onChange({ kind: 'added', node }, m))
m.removedNodes.forEach((node) =>
onChange({ kind: 'removed', node }, m),
)
} else if (m.type === 'attributes') {
const el = m.target as Element
onChange(
{
kind: 'attr',
target: el,
name: m.attributeName,
oldValue: m.oldValue ?? null,
},
m,
)
} else {
// If someone mutates a Text node inside <title>, surface it as a title change.
const isInTitle =
m.target.parentNode &&
(m.target.parentNode as Element).tagName.toLowerCase() === 'title'
if (isInTitle) onChange({ kind: 'title', title: document.title }, m)
}
}
})
headObserver.observe(document.head, {
childList,
attributes,
subtree,
attributeOldValue: attributes,
characterData: true, // helps catch <title> text node edits
characterDataOldValue: false,
})
// Extra explicit observer for <title>, since `document.title = "..."`
// may not always bubble as a head mutation in all setups.
let titleObserver: MutationObserver | undefined
if (observeTitle) {
const titleEl =
document.head.querySelector('title') ||
// create a <title> if missing so future changes are observable
document.head.appendChild(document.createElement('title'))
titleObserver = new MutationObserver(() => {
onChange({ kind: 'title', title: document.title })
})
titleObserver.observe(titleEl, {
childList: true,
characterData: true,
subtree: true,
})
}
onCleanup(() => {
headObserver.disconnect()
titleObserver?.disconnect()
})
})
}