@creativecreate/react-footnotes 1.0.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/README.md +426 -0
- package/dist/index.d.mts +155 -0
- package/dist/index.d.ts +155 -0
- package/dist/index.js +285 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +277 -0
- package/dist/index.mjs.map +1 -0
- package/footnotes.css +106 -0
- package/messages.json +11 -0
- package/package.json +38 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
type FootnotesCategory = 'citation' | 'special';
|
|
5
|
+
type FootnoteKeys = string;
|
|
6
|
+
type FootnoteProps = {
|
|
7
|
+
id: FootnoteKeys;
|
|
8
|
+
type?: FootnotesCategory;
|
|
9
|
+
children?: ReactNode;
|
|
10
|
+
};
|
|
11
|
+
type FootnoteMessages = {
|
|
12
|
+
citation?: Record<string, ReactNode>;
|
|
13
|
+
special?: Record<string, ReactNode>;
|
|
14
|
+
};
|
|
15
|
+
type FootnoteContextType = {
|
|
16
|
+
registerFootnote: (footnote: FootnoteProps) => void;
|
|
17
|
+
citationList: FootnoteProps[];
|
|
18
|
+
specialList: FootnoteProps[];
|
|
19
|
+
clickHandler: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
|
20
|
+
getContent: (id: string, type: FootnotesCategory) => ReactNode | null;
|
|
21
|
+
};
|
|
22
|
+
declare const splitFootnotes: (footnotes: FootnoteProps[]) => {
|
|
23
|
+
specialList: FootnoteProps[];
|
|
24
|
+
citationList: FootnoteProps[];
|
|
25
|
+
};
|
|
26
|
+
declare const getSpecialChar: (footnoteList: FootnoteProps[], id: FootnoteKeys) => string;
|
|
27
|
+
|
|
28
|
+
type FootnoteProviderProps = {
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
/**
|
|
31
|
+
* Messages object containing footnote content.
|
|
32
|
+
* Structure: { citation: { id: content }, special: { id: content } }
|
|
33
|
+
*/
|
|
34
|
+
messages?: FootnoteMessages;
|
|
35
|
+
/**
|
|
36
|
+
* Current pathname string for route-based footnote reset.
|
|
37
|
+
* For React Router: `\`${location.pathname}${location.search}\``
|
|
38
|
+
* For simple cases: `\`${window.location.pathname}${window.location.search}\``
|
|
39
|
+
*/
|
|
40
|
+
pathname: string;
|
|
41
|
+
};
|
|
42
|
+
declare const FootnoteProvider: ({ children, messages, pathname, }: FootnoteProviderProps) => react_jsx_runtime.JSX.Element;
|
|
43
|
+
declare const useFootnotes: () => FootnoteContextType;
|
|
44
|
+
|
|
45
|
+
type FootnoteListProps = {
|
|
46
|
+
className?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Optional className for the list item
|
|
49
|
+
*/
|
|
50
|
+
itemClassName?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Optional className for the <sup> element
|
|
53
|
+
*/
|
|
54
|
+
itemSupClassName?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Optional className for the content element
|
|
57
|
+
*/
|
|
58
|
+
itemContentClassName?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Optional className for the back link button
|
|
61
|
+
*/
|
|
62
|
+
itemBackLinkClassName?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Optional className for the back link icon
|
|
65
|
+
*/
|
|
66
|
+
itemBackLinkIconClassName?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Order of footnotes in the list
|
|
69
|
+
* @default 'special-first' - Special footnotes appear first, then citations
|
|
70
|
+
* 'citation-first' - Citations appear first, then special footnotes
|
|
71
|
+
*/
|
|
72
|
+
order?: 'special-first' | 'citation-first';
|
|
73
|
+
};
|
|
74
|
+
declare const FootnoteList: ({ className, itemClassName, itemSupClassName, itemContentClassName, itemBackLinkClassName, itemBackLinkIconClassName, order, }: FootnoteListProps) => react_jsx_runtime.JSX.Element | null;
|
|
75
|
+
|
|
76
|
+
interface FootnoteListItemProps {
|
|
77
|
+
id: FootnoteKeys;
|
|
78
|
+
type?: 'citation' | 'special';
|
|
79
|
+
symbol: string;
|
|
80
|
+
children?: React.ReactNode;
|
|
81
|
+
/**
|
|
82
|
+
* Optional className to override default styles
|
|
83
|
+
*/
|
|
84
|
+
className?: string;
|
|
85
|
+
/**
|
|
86
|
+
* Optional className for the <sup> element
|
|
87
|
+
*/
|
|
88
|
+
supClassName?: string;
|
|
89
|
+
/**
|
|
90
|
+
* Optional className for the content element
|
|
91
|
+
*/
|
|
92
|
+
contentClassName?: string;
|
|
93
|
+
/**
|
|
94
|
+
* Optional className for the back link button
|
|
95
|
+
*/
|
|
96
|
+
backLinkClassName?: string;
|
|
97
|
+
/**
|
|
98
|
+
* Optional className for the back link icon
|
|
99
|
+
*/
|
|
100
|
+
backLinkIconClassName?: string;
|
|
101
|
+
}
|
|
102
|
+
declare const FootnoteListItem: ({ id, children, symbol, className, supClassName, contentClassName, backLinkClassName, backLinkIconClassName, }: FootnoteListItemProps) => react_jsx_runtime.JSX.Element;
|
|
103
|
+
|
|
104
|
+
type FootnoteComponentProps = {
|
|
105
|
+
id: FootnoteKeys;
|
|
106
|
+
/**
|
|
107
|
+
* Type of footnote: 'citation' or 'special'
|
|
108
|
+
* Required when using messages from FootnoteProvider
|
|
109
|
+
*/
|
|
110
|
+
type: FootnotesCategory;
|
|
111
|
+
/**
|
|
112
|
+
* Optional function to get footnote content.
|
|
113
|
+
* If provided, will override messages from FootnoteProvider.
|
|
114
|
+
* Should return the content for the footnote with the given id and type.
|
|
115
|
+
*/
|
|
116
|
+
getContent?: (id: string, type: FootnotesCategory) => React.ReactNode;
|
|
117
|
+
/**
|
|
118
|
+
* Optional children to use as footnote content.
|
|
119
|
+
* If provided, will be used instead of getContent or messages.
|
|
120
|
+
*/
|
|
121
|
+
children?: React.ReactNode;
|
|
122
|
+
/**
|
|
123
|
+
* Optional className to override default styles
|
|
124
|
+
*/
|
|
125
|
+
className?: string;
|
|
126
|
+
/**
|
|
127
|
+
* Optional className for the <sup> element
|
|
128
|
+
*/
|
|
129
|
+
supClassName?: string;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Footnote component - renders an inline footnote reference.
|
|
134
|
+
*
|
|
135
|
+
* Priority for content:
|
|
136
|
+
* 1. If `children` is provided, it will be used for the footnote content (highest priority).
|
|
137
|
+
* 2. If `getContent` prop is passed, it is called with `id` and `type` and its result is used.
|
|
138
|
+
* 3. Otherwise, content is fetched from messages via FootnoteProvider context (`getContentFromContext`).
|
|
139
|
+
*
|
|
140
|
+
* Registers itself in the footnote context via `registerFootnote` when content exists.
|
|
141
|
+
*
|
|
142
|
+
* Displays a symbol (number for 'citation', special character for 'special').
|
|
143
|
+
* Clicking the reference will navigate to its corresponding item in the FootnoteList.
|
|
144
|
+
*
|
|
145
|
+
* Props:
|
|
146
|
+
* - id: Unique id for the footnote
|
|
147
|
+
* - type: "citation" | "special"
|
|
148
|
+
* - getContent?: (id, type) => content (overrides messages)
|
|
149
|
+
* - children?: ReactNode (overrides everything, highest priority)
|
|
150
|
+
* - className?: class for the button/footnote ref
|
|
151
|
+
* - supClassName?: class for the <sup> element
|
|
152
|
+
*/
|
|
153
|
+
declare const Footnote: ({ id, type, getContent, children, className, supClassName, }: FootnoteComponentProps) => react_jsx_runtime.JSX.Element | null;
|
|
154
|
+
|
|
155
|
+
export { Footnote, type FootnoteKeys, FootnoteList, FootnoteListItem, type FootnoteMessages, type FootnoteProps, FootnoteProvider, type FootnotesCategory, getSpecialChar, splitFootnotes, useFootnotes };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
|
|
6
|
+
// src/FootnoteProvider.tsx
|
|
7
|
+
var splitFootnotes = (footnotes) => {
|
|
8
|
+
const specialList = footnotes.filter(
|
|
9
|
+
(footnote) => footnote.type === "special"
|
|
10
|
+
);
|
|
11
|
+
const citationList = footnotes.filter(
|
|
12
|
+
(footnote) => footnote.type === "citation"
|
|
13
|
+
);
|
|
14
|
+
return {
|
|
15
|
+
specialList,
|
|
16
|
+
citationList
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
var getSpecialChar = (footnoteList, id) => {
|
|
20
|
+
const symbols = ["*", "\u2020", "\u2021", "\xA7", "\xB6", "#", "\u2016", "\u2042", "\u2051", "\u203B", "\u25CA", "\xA4"];
|
|
21
|
+
const specialFootnotes = footnoteList.filter((f) => f.type === "special");
|
|
22
|
+
const index = specialFootnotes.findIndex((f) => f.id === id);
|
|
23
|
+
return symbols[index] || "";
|
|
24
|
+
};
|
|
25
|
+
var FootnoteContext = react.createContext(
|
|
26
|
+
void 0
|
|
27
|
+
);
|
|
28
|
+
var FootnoteProvider = ({
|
|
29
|
+
children,
|
|
30
|
+
messages,
|
|
31
|
+
pathname
|
|
32
|
+
}) => {
|
|
33
|
+
const [footnotes, setFootnotes] = react.useState([]);
|
|
34
|
+
const activeClick = react.useRef(null);
|
|
35
|
+
const previousPathnameRef = react.useRef(null);
|
|
36
|
+
const getContent = react.useCallback((id, type) => {
|
|
37
|
+
if (!messages) return null;
|
|
38
|
+
if (type === "special" && messages.special?.[id]) {
|
|
39
|
+
return messages.special[id];
|
|
40
|
+
}
|
|
41
|
+
if (type === "citation" && messages.citation?.[id]) {
|
|
42
|
+
return messages.citation[id];
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}, [messages]);
|
|
46
|
+
react.useEffect(() => {
|
|
47
|
+
if (previousPathnameRef.current !== null && previousPathnameRef.current !== pathname) {
|
|
48
|
+
setFootnotes([]);
|
|
49
|
+
}
|
|
50
|
+
previousPathnameRef.current = pathname;
|
|
51
|
+
}, [pathname]);
|
|
52
|
+
const registerFootnote = react.useCallback((footnote) => {
|
|
53
|
+
setFootnotes((prev) => {
|
|
54
|
+
const index = prev.findIndex(
|
|
55
|
+
(f) => f.id === footnote.id && f.type === footnote.type
|
|
56
|
+
);
|
|
57
|
+
if (index === -1) {
|
|
58
|
+
return [...prev, footnote];
|
|
59
|
+
}
|
|
60
|
+
return prev;
|
|
61
|
+
});
|
|
62
|
+
}, []);
|
|
63
|
+
const clickHandler = (e) => {
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
const id = e.currentTarget.getAttribute("data-anchor") || "";
|
|
66
|
+
const isRefClick = id?.includes("footnote-");
|
|
67
|
+
let target = null;
|
|
68
|
+
if (isRefClick) {
|
|
69
|
+
activeClick.current = e.currentTarget;
|
|
70
|
+
}
|
|
71
|
+
if (!isRefClick && activeClick.current?.dataset?.id === id) {
|
|
72
|
+
target = activeClick.current;
|
|
73
|
+
activeClick.current = null;
|
|
74
|
+
} else if (!isRefClick) {
|
|
75
|
+
target = document.querySelector(`[data-id="${id}"]`);
|
|
76
|
+
activeClick.current = null;
|
|
77
|
+
} else {
|
|
78
|
+
target = document.getElementById(id);
|
|
79
|
+
}
|
|
80
|
+
if (target) {
|
|
81
|
+
const animationClass = ["underline", "animate-underline-flash"];
|
|
82
|
+
target.scrollIntoView({ behavior: "smooth" });
|
|
83
|
+
target.classList.remove(...animationClass);
|
|
84
|
+
void target.offsetWidth;
|
|
85
|
+
requestAnimationFrame(() => {
|
|
86
|
+
target.classList.add(...animationClass);
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
target.classList.remove(...animationClass);
|
|
89
|
+
}, 1500);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
94
|
+
FootnoteContext.Provider,
|
|
95
|
+
{
|
|
96
|
+
value: { registerFootnote, ...splitFootnotes(footnotes), clickHandler, getContent },
|
|
97
|
+
children
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
var useFootnotes = () => {
|
|
102
|
+
const context = react.useContext(FootnoteContext);
|
|
103
|
+
if (!context) {
|
|
104
|
+
throw new Error("useFootnotes must be used within a FootnoteProvider");
|
|
105
|
+
}
|
|
106
|
+
return context;
|
|
107
|
+
};
|
|
108
|
+
var FootnoteListItem = ({
|
|
109
|
+
id,
|
|
110
|
+
children,
|
|
111
|
+
symbol,
|
|
112
|
+
className,
|
|
113
|
+
supClassName,
|
|
114
|
+
contentClassName,
|
|
115
|
+
backLinkClassName,
|
|
116
|
+
backLinkIconClassName
|
|
117
|
+
}) => {
|
|
118
|
+
const { clickHandler } = useFootnotes();
|
|
119
|
+
const [hasReference, setHasReference] = react.useState(false);
|
|
120
|
+
react.useEffect(() => {
|
|
121
|
+
const checkReference = () => {
|
|
122
|
+
const reference = document.querySelector(`[data-id="ref-${id}"]`);
|
|
123
|
+
const isVisible = reference && !reference.closest(".hidden");
|
|
124
|
+
setHasReference(!!isVisible);
|
|
125
|
+
};
|
|
126
|
+
checkReference();
|
|
127
|
+
const mainElement = document.querySelector("main") || document.body;
|
|
128
|
+
const observer = new MutationObserver(checkReference);
|
|
129
|
+
observer.observe(mainElement, {
|
|
130
|
+
childList: true,
|
|
131
|
+
subtree: true
|
|
132
|
+
});
|
|
133
|
+
return () => observer.disconnect();
|
|
134
|
+
}, [id]);
|
|
135
|
+
const symbolId = `footnote-symbol-${id}`;
|
|
136
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
137
|
+
"li",
|
|
138
|
+
{
|
|
139
|
+
className: className || "footnote-list-item",
|
|
140
|
+
role: "doc-endnote",
|
|
141
|
+
children: [
|
|
142
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
143
|
+
"sup",
|
|
144
|
+
{
|
|
145
|
+
id: symbolId,
|
|
146
|
+
className: supClassName || "footnote-list-item__symbol",
|
|
147
|
+
"aria-label": `Footnote ${symbol}`,
|
|
148
|
+
children: symbol
|
|
149
|
+
}
|
|
150
|
+
),
|
|
151
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
152
|
+
"span",
|
|
153
|
+
{
|
|
154
|
+
id: `footnote-${id}`,
|
|
155
|
+
className: contentClassName || "footnote-list-item__content",
|
|
156
|
+
"aria-labelledby": symbolId,
|
|
157
|
+
children
|
|
158
|
+
}
|
|
159
|
+
),
|
|
160
|
+
" ",
|
|
161
|
+
hasReference && /* @__PURE__ */ jsxRuntime.jsx(
|
|
162
|
+
"button",
|
|
163
|
+
{
|
|
164
|
+
onClick: (e) => clickHandler(e),
|
|
165
|
+
"aria-label": `Back to reference ${symbol} in text`,
|
|
166
|
+
title: `Back to reference ${symbol}`,
|
|
167
|
+
"data-anchor": `ref-${id}`,
|
|
168
|
+
"data-testid": `back-to-reference-${id}`,
|
|
169
|
+
className: backLinkClassName || "footnote-list-item__back-link",
|
|
170
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
171
|
+
"small",
|
|
172
|
+
{
|
|
173
|
+
className: backLinkIconClassName || "footnote-list-item__back-link-icon",
|
|
174
|
+
"aria-hidden": "true",
|
|
175
|
+
children: "\u21A9"
|
|
176
|
+
}
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
]
|
|
181
|
+
},
|
|
182
|
+
id
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
var FootnoteList = ({
|
|
186
|
+
className,
|
|
187
|
+
itemClassName,
|
|
188
|
+
itemSupClassName,
|
|
189
|
+
itemContentClassName,
|
|
190
|
+
itemBackLinkClassName,
|
|
191
|
+
itemBackLinkIconClassName,
|
|
192
|
+
order = "special-first"
|
|
193
|
+
}) => {
|
|
194
|
+
const { specialList, citationList } = useFootnotes();
|
|
195
|
+
if (!specialList.length && !citationList.length) return null;
|
|
196
|
+
const specialFootnotes = specialList.map((footnote) => ({
|
|
197
|
+
id: footnote.id,
|
|
198
|
+
children: footnote.children,
|
|
199
|
+
type: footnote.type,
|
|
200
|
+
symbol: getSpecialChar(specialList, footnote.id)
|
|
201
|
+
}));
|
|
202
|
+
const citationFootnotes = citationList.map((footnote, index) => ({
|
|
203
|
+
id: footnote.id,
|
|
204
|
+
children: footnote.children,
|
|
205
|
+
type: footnote.type,
|
|
206
|
+
symbol: `${index + 1}`
|
|
207
|
+
}));
|
|
208
|
+
const allFootnotes = order === "citation-first" ? [...citationFootnotes, ...specialFootnotes] : [...specialFootnotes, ...citationFootnotes];
|
|
209
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
210
|
+
"ol",
|
|
211
|
+
{
|
|
212
|
+
className: className || "footnote-list",
|
|
213
|
+
role: "doc-endnotes",
|
|
214
|
+
"aria-label": "Footnotes",
|
|
215
|
+
children: allFootnotes.map((footnote) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
216
|
+
FootnoteListItem,
|
|
217
|
+
{
|
|
218
|
+
id: footnote.id,
|
|
219
|
+
type: footnote.type,
|
|
220
|
+
symbol: footnote.symbol,
|
|
221
|
+
className: itemClassName,
|
|
222
|
+
supClassName: itemSupClassName,
|
|
223
|
+
contentClassName: itemContentClassName,
|
|
224
|
+
backLinkClassName: itemBackLinkClassName,
|
|
225
|
+
backLinkIconClassName: itemBackLinkIconClassName,
|
|
226
|
+
children: footnote.children
|
|
227
|
+
},
|
|
228
|
+
`footnote-${footnote.type}-${footnote.id}`
|
|
229
|
+
))
|
|
230
|
+
}
|
|
231
|
+
);
|
|
232
|
+
};
|
|
233
|
+
var Footnote = ({
|
|
234
|
+
id,
|
|
235
|
+
type,
|
|
236
|
+
getContent,
|
|
237
|
+
children,
|
|
238
|
+
className,
|
|
239
|
+
supClassName
|
|
240
|
+
}) => {
|
|
241
|
+
const { registerFootnote, specialList, citationList, clickHandler, getContent: getContentFromContext } = useFootnotes();
|
|
242
|
+
let content = null;
|
|
243
|
+
if (children) {
|
|
244
|
+
content = children;
|
|
245
|
+
} else if (getContent) {
|
|
246
|
+
content = getContent(id, type);
|
|
247
|
+
} else if (getContentFromContext) {
|
|
248
|
+
content = getContentFromContext(id, type);
|
|
249
|
+
}
|
|
250
|
+
react.useEffect(() => {
|
|
251
|
+
if (type && content) {
|
|
252
|
+
registerFootnote({ id, children: content, type });
|
|
253
|
+
} else if (!content) {
|
|
254
|
+
console.warn(`Footnote ${id} has no content. Provide messages to FootnoteProvider, getContent function, or children.`);
|
|
255
|
+
}
|
|
256
|
+
}, [id, type, content, registerFootnote]);
|
|
257
|
+
if (!content) return null;
|
|
258
|
+
const specialSymbol = type === "special" ? getSpecialChar(specialList, id) : "";
|
|
259
|
+
const citationSymbol = type === "citation" ? citationList.findIndex((f) => f.id === id) : -1;
|
|
260
|
+
const symbol = type === "citation" ? `${citationSymbol + 1}` : specialSymbol;
|
|
261
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
262
|
+
"button",
|
|
263
|
+
{
|
|
264
|
+
onClick: (e) => clickHandler(e),
|
|
265
|
+
"aria-label": `Footnote ${symbol}: See note ${symbol}`,
|
|
266
|
+
"aria-describedby": `footnote-${id}`,
|
|
267
|
+
role: "doc-noteref",
|
|
268
|
+
className: className || "footnote-ref",
|
|
269
|
+
"data-id": `ref-${id}`,
|
|
270
|
+
"data-anchor": `footnote-${id}`,
|
|
271
|
+
"data-testid": `footnote-${id}`,
|
|
272
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("sup", { className: supClassName || "footnote-symbol", "aria-hidden": "true", children: symbol })
|
|
273
|
+
}
|
|
274
|
+
);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
exports.Footnote = Footnote;
|
|
278
|
+
exports.FootnoteList = FootnoteList;
|
|
279
|
+
exports.FootnoteListItem = FootnoteListItem;
|
|
280
|
+
exports.FootnoteProvider = FootnoteProvider;
|
|
281
|
+
exports.getSpecialChar = getSpecialChar;
|
|
282
|
+
exports.splitFootnotes = splitFootnotes;
|
|
283
|
+
exports.useFootnotes = useFootnotes;
|
|
284
|
+
//# sourceMappingURL=index.js.map
|
|
285
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/FootnoteProvider.tsx","../src/FootnoteListItem.tsx","../src/FootnoteList.tsx","../src/Footnote.tsx"],"names":["createContext","useState","useRef","useCallback","useEffect","jsx","useContext","jsxs"],"mappings":"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","file":"index.js","sourcesContent":["'use client';\nimport {\n createContext,\n useContext,\n useState,\n ReactNode,\n useEffect,\n useCallback,\n useRef,\n} from 'react';\n\nexport type FootnotesCategory = 'citation' | 'special';\nexport type FootnoteKeys = string;\nexport type FootnoteProps = {\n id: FootnoteKeys;\n type?: FootnotesCategory;\n children?: ReactNode;\n};\n\ntype FootnoteMessages = {\n citation?: Record<string, ReactNode>;\n special?: Record<string, ReactNode>;\n};\n\ntype FootnoteContextType = {\n registerFootnote: (footnote: FootnoteProps) => void;\n citationList: FootnoteProps[];\n specialList: FootnoteProps[];\n clickHandler: (e: React.MouseEvent<HTMLButtonElement>) => void;\n getContent: (id: string, type: FootnotesCategory) => ReactNode | null;\n};\n\nexport const splitFootnotes = (footnotes: FootnoteProps[]) => {\n const specialList = footnotes.filter(\n (footnote) => footnote.type === 'special',\n );\n const citationList = footnotes.filter(\n (footnote) => footnote.type === 'citation',\n );\n return {\n specialList,\n citationList,\n };\n};\n\nexport const getSpecialChar = (\n footnoteList: FootnoteProps[],\n id: FootnoteKeys,\n) => {\n const symbols = ['*', '†', '‡', '§', '¶', '#', '‖', '⁂', '⁑', '※', '◊', '¤'];\n // Get all previous special footnotes before this one\n const specialFootnotes = footnoteList.filter((f) => f.type === 'special');\n const index = specialFootnotes.findIndex((f) => f.id === id);\n return symbols[index] || '';\n};\n\nexport type { FootnoteMessages };\n\ntype FootnoteProviderProps = {\n children: ReactNode;\n /**\n * Messages object containing footnote content.\n * Structure: { citation: { id: content }, special: { id: content } }\n */\n messages?: FootnoteMessages;\n /**\n * Current pathname string for route-based footnote reset.\n * For React Router: `\\`${location.pathname}${location.search}\\``\n * For simple cases: `\\`${window.location.pathname}${window.location.search}\\``\n */\n pathname: string;\n};\n\nconst FootnoteContext = createContext<FootnoteContextType | undefined>(\n undefined,\n);\n\nexport const FootnoteProvider = ({ \n children,\n messages,\n pathname,\n}: FootnoteProviderProps) => {\n const [footnotes, setFootnotes] = useState<FootnoteProps[]>([]);\n const activeClick = useRef<HTMLButtonElement>(null);\n const previousPathnameRef = useRef<string | null>(null);\n\n // Function to get content from messages\n const getContent = useCallback((id: string, type: FootnotesCategory): ReactNode | null => {\n if (!messages) return null;\n \n if (type === 'special' && messages.special?.[id]) {\n return messages.special[id];\n }\n \n if (type === 'citation' && messages.citation?.[id]) {\n return messages.citation[id];\n }\n \n return null;\n }, [messages]);\n\n // Reset footnotes when route changes\n useEffect(() => {\n // Only reset if pathname actually changed (not on initial mount)\n if (previousPathnameRef.current !== null && previousPathnameRef.current !== pathname) {\n setFootnotes([]);\n }\n previousPathnameRef.current = pathname;\n }, [pathname]);\n\n // update footnotes\n const registerFootnote = useCallback((footnote: FootnoteProps) => {\n setFootnotes((prev) => {\n // Check if a footnote with the same id AND type already exists\n const index = prev.findIndex(\n (f) => f.id === footnote.id && f.type === footnote.type\n );\n // new (either new id or same id but different type)\n if (index === -1) {\n return [...prev, footnote];\n }\n // duplicate (same id and same type)\n return prev;\n });\n }, []);\n\n // handle all footnote click events\n // handle \"footnote <sup>\" clicks (isRefClick = true)\n // - only single target instance, no duplicated targets\n // - when clicking on a \"footnote ref\", navigate to the matching footnote list item\n // handle \"back to ref\" button clicks (isRefClick = false)\n // - can have duplicate targets\n // - when clicking on the \"back to ref\", track and point to the active ref target\n // - when clicking on a random \"back to ref\" link, navigate to the first instance\n const clickHandler = (e: React.MouseEvent<HTMLButtonElement>) => {\n e.preventDefault();\n\n // get the id from the data-anchor - 'ref-<id>' or 'footnote-<id>'\n const id = e.currentTarget.getAttribute('data-anchor') || '';\n const isRefClick = id?.includes('footnote-');\n let target: HTMLButtonElement | null = null;\n\n // set active ref\n if (isRefClick) {\n activeClick.current = e.currentTarget;\n }\n\n // handle \"back to ref\" link clicks. point to active ref\n if (!isRefClick && activeClick.current?.dataset?.id === id) {\n target = activeClick.current;\n activeClick.current = null;\n } else if (!isRefClick) {\n // handle random \"back to ref\" link clicks. point to first instance\n target = document.querySelector(`[data-id=\"${id}\"]`);\n activeClick.current = null;\n } else {\n // handle \"footnote <sup>\" clicks. point to target\n target = document.getElementById(id) as HTMLButtonElement;\n }\n\n if (target) {\n const animationClass = ['underline', 'animate-underline-flash'];\n // scroll to target\n target.scrollIntoView({ behavior: 'smooth' });\n // remove existing animation class\n target.classList.remove(...animationClass);\n // Force a reflow to ensure removal is processed\n void target.offsetWidth;\n // Schedule the class addition for the next animation frame\n requestAnimationFrame(() => {\n target.classList.add(...animationClass);\n // Remove classes after animation completes (0.6s to match CSS animation duration)\n setTimeout(() => {\n target.classList.remove(...animationClass);\n }, 1500);\n });\n }\n };\n\n return (\n <FootnoteContext.Provider\n value={{ registerFootnote, ...splitFootnotes(footnotes), clickHandler, getContent }}\n >\n {children}\n </FootnoteContext.Provider>\n );\n};\n\nexport const useFootnotes = () => {\n const context = useContext(FootnoteContext);\n if (!context) {\n throw new Error('useFootnotes must be used within a FootnoteProvider');\n }\n return context;\n};\n","'use client';\nimport { FootnoteKeys, useFootnotes } from './FootnoteProvider';\nimport { useEffect, useState } from 'react';\n\ninterface FootnoteListItemProps {\n id: FootnoteKeys;\n type?: 'citation' | 'special';\n symbol: string;\n children?: React.ReactNode;\n /**\n * Optional className to override default styles\n */\n className?: string;\n /**\n * Optional className for the <sup> element\n */\n supClassName?: string;\n /**\n * Optional className for the content element\n */\n contentClassName?: string;\n /**\n * Optional className for the back link button\n */\n backLinkClassName?: string;\n /**\n * Optional className for the back link icon\n */\n backLinkIconClassName?: string;\n}\n\nexport const FootnoteListItem = ({\n id,\n children,\n symbol,\n className,\n supClassName,\n contentClassName,\n backLinkClassName,\n backLinkIconClassName,\n}: FootnoteListItemProps) => {\n const { clickHandler } = useFootnotes();\n const [hasReference, setHasReference] = useState(false);\n\n // Check if the reference exists in the DOM\n useEffect(() => {\n const checkReference = () => {\n const reference = document.querySelector(`[data-id=\"ref-${id}\"]`);\n // if elm exists and none of its parents are hidden\n const isVisible = reference && !reference.closest('.hidden');\n setHasReference(!!isVisible);\n };\n\n // Initial check\n checkReference();\n\n // Set up a mutation observer to watch for DOM changes in the main element only\n const mainElement = document.querySelector('main') || document.body;\n const observer = new MutationObserver(checkReference);\n observer.observe(mainElement, {\n childList: true,\n subtree: true,\n });\n return () => observer.disconnect();\n }, [id]);\n\n const symbolId = `footnote-symbol-${id}`;\n\n return (\n <li\n className={className || 'footnote-list-item'}\n key={id}\n role=\"doc-endnote\"\n >\n {/* footnote reference */}\n <sup \n id={symbolId}\n className={supClassName || 'footnote-list-item__symbol'}\n aria-label={`Footnote ${symbol}`}\n >\n {symbol}\n </sup>\n <span\n id={`footnote-${id}`}\n className={contentClassName || 'footnote-list-item__content'}\n aria-labelledby={symbolId}\n >\n {children}\n </span>\n {' '}\n\n {/* back to reference link */}\n {hasReference && (\n <button\n onClick={(e) => clickHandler(e)}\n aria-label={`Back to reference ${symbol} in text`}\n title={`Back to reference ${symbol}`}\n data-anchor={`ref-${id}`}\n data-testid={`back-to-reference-${id}`}\n className={backLinkClassName || 'footnote-list-item__back-link'}\n >\n <small \n className={backLinkIconClassName || \"footnote-list-item__back-link-icon\"}\n aria-hidden=\"true\"\n >\n ↩\n </small>\n </button>\n )}\n </li>\n );\n};\n","'use client';\nimport {\n getSpecialChar,\n useFootnotes,\n} from './FootnoteProvider';\nimport { FootnoteListItem } from './FootnoteListItem';\n\ntype FootnoteListProps = {\n // Optional className for the wrapper\n className?: string;\n /**\n * Optional className for the list item\n */\n itemClassName?: string;\n /**\n * Optional className for the <sup> element\n */\n itemSupClassName?: string;\n /**\n * Optional className for the content element\n */\n itemContentClassName?: string;\n /**\n * Optional className for the back link button\n */\n itemBackLinkClassName?: string;\n /**\n * Optional className for the back link icon\n */\n itemBackLinkIconClassName?: string;\n /**\n * Order of footnotes in the list\n * @default 'special-first' - Special footnotes appear first, then citations\n * 'citation-first' - Citations appear first, then special footnotes\n */\n order?: 'special-first' | 'citation-first';\n};\n\nexport const FootnoteList = ({ \n className,\n itemClassName,\n itemSupClassName,\n itemContentClassName,\n itemBackLinkClassName,\n itemBackLinkIconClassName,\n order = 'special-first',\n}: FootnoteListProps) => {\n const { specialList, citationList } = useFootnotes();\n\n // exit if no footnotes\n if (!specialList.length && !citationList.length) return null;\n\n // Map footnotes with their symbols\n const specialFootnotes = specialList.map((footnote) => ({\n id: footnote.id,\n children: footnote.children,\n type: footnote.type,\n symbol: getSpecialChar(specialList, footnote.id),\n }));\n\n const citationFootnotes = citationList.map((footnote, index) => ({\n id: footnote.id,\n children: footnote.children,\n type: footnote.type,\n symbol: `${index + 1}`,\n }));\n\n // Combine footnotes based on order prop\n const allFootnotes = order === 'citation-first'\n ? [...citationFootnotes, ...specialFootnotes]\n : [...specialFootnotes, ...citationFootnotes];\n\n return (\n <ol \n className={className || \"footnote-list\"}\n role=\"doc-endnotes\"\n aria-label=\"Footnotes\"\n >\n {allFootnotes.map((footnote) => (\n <FootnoteListItem\n key={`footnote-${footnote.type}-${footnote.id}`}\n id={footnote.id}\n type={footnote.type}\n symbol={footnote.symbol}\n className={itemClassName}\n supClassName={itemSupClassName}\n contentClassName={itemContentClassName}\n backLinkClassName={itemBackLinkClassName}\n backLinkIconClassName={itemBackLinkIconClassName}\n >\n {footnote.children}\n </FootnoteListItem>\n ))}\n </ol>\n );\n};\n","'use client';\nimport { useEffect } from 'react';\nimport {\n useFootnotes,\n FootnoteKeys,\n FootnotesCategory,\n getSpecialChar,\n} from './FootnoteProvider';\n\ntype FootnoteComponentProps = {\n id: FootnoteKeys;\n /**\n * Type of footnote: 'citation' or 'special'\n * Required when using messages from FootnoteProvider\n */\n type: FootnotesCategory;\n /**\n * Optional function to get footnote content.\n * If provided, will override messages from FootnoteProvider.\n * Should return the content for the footnote with the given id and type.\n */\n getContent?: (id: string, type: FootnotesCategory) => React.ReactNode;\n /**\n * Optional children to use as footnote content.\n * If provided, will be used instead of getContent or messages.\n */\n children?: React.ReactNode;\n /**\n * Optional className to override default styles\n */\n className?: string;\n /**\n * Optional className for the <sup> element\n */\n supClassName?: string;\n};\n\n// Re-export for convenience\nexport type { FootnoteKeys } from './FootnoteProvider';\n\n/**\n * Footnote component - renders an inline footnote reference.\n * \n * Priority for content:\n * 1. If `children` is provided, it will be used for the footnote content (highest priority).\n * 2. If `getContent` prop is passed, it is called with `id` and `type` and its result is used.\n * 3. Otherwise, content is fetched from messages via FootnoteProvider context (`getContentFromContext`).\n * \n * Registers itself in the footnote context via `registerFootnote` when content exists.\n * \n * Displays a symbol (number for 'citation', special character for 'special').\n * Clicking the reference will navigate to its corresponding item in the FootnoteList.\n * \n * Props:\n * - id: Unique id for the footnote\n * - type: \"citation\" | \"special\"\n * - getContent?: (id, type) => content (overrides messages)\n * - children?: ReactNode (overrides everything, highest priority)\n * - className?: class for the button/footnote ref\n * - supClassName?: class for the <sup> element\n */\nexport const Footnote = ({ \n id, \n type,\n getContent, \n children,\n className,\n supClassName,\n}: FootnoteComponentProps) => {\n const { registerFootnote, specialList, citationList, clickHandler, getContent: getContentFromContext } =\n useFootnotes();\n\n let content: React.ReactNode = null;\n if (children) {\n content = children;\n } else if (getContent) {\n content = getContent(id, type);\n } else if (getContentFromContext) {\n content = getContentFromContext(id, type);\n }\n\n useEffect(() => {\n if (type && content) {\n registerFootnote({ id, children: content, type });\n } else if (!content) {\n console.warn(`Footnote ${id} has no content. Provide messages to FootnoteProvider, getContent function, or children.`);\n }\n }, [id, type, content, registerFootnote]);\n\n // exit if no content\n if (!content) return null;\n\n // create symbol\n const specialSymbol = type === 'special' ? getSpecialChar(specialList, id) : '';\n const citationSymbol = type === 'citation'\n ? citationList.findIndex((f) => f.id === id)\n : -1;\n const symbol = type === 'citation' ? `${citationSymbol + 1}` : specialSymbol;\n\n return (\n <button\n onClick={(e) => clickHandler(e)}\n aria-label={`Footnote ${symbol}: See note ${symbol}`}\n aria-describedby={`footnote-${id}`}\n role=\"doc-noteref\"\n className={className || 'footnote-ref'}\n data-id={`ref-${id}`}\n data-anchor={`footnote-${id}`}\n data-testid={`footnote-${id}`}\n >\n <sup className={supClassName || \"footnote-symbol\"} aria-hidden=\"true\">{symbol}</sup>\n </button>\n );\n};\n"]}
|