Skip to content

Commit b0e0b65

Browse files
FEATURE: Bundle discourse-footnote plugin into core (#23995)
Formerly https://github.com/discourse/discourse-footnote
1 parent 043b4a4 commit b0e0b65

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+1256
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
!/plugins/poll/
4444
!/plugins/styleguide
4545
!/plugins/checklist/
46+
!/plugins/footnote/
4647
/plugins/*/auto_generated/
4748

4849
/spec/fixtures/plugins/my_plugin/auto_generated

plugins/footnote/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
## Discourse footnotes plugin
2+
3+
https://meta.discourse.org/t/discourse-footnote/84533
4+
5+
Official footnotes Discourse plugin
6+
7+
Based off: [github.com/markdown-it/markdown-it-footnote](https://github.com/markdown-it/markdown-it-footnote)
8+
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { createPopper } from "@popperjs/core";
2+
import { withPluginApi } from "discourse/lib/plugin-api";
3+
import { iconHTML } from "discourse-common/lib/icon-library";
4+
5+
let inlineFootnotePopper;
6+
7+
function applyInlineFootnotes(elem) {
8+
const footnoteRefs = elem.querySelectorAll("sup.footnote-ref");
9+
10+
footnoteRefs.forEach((footnoteRef) => {
11+
const refLink = footnoteRef.querySelector("a");
12+
if (!refLink) {
13+
return;
14+
}
15+
16+
const expandableFootnote = document.createElement("a");
17+
expandableFootnote.classList.add("expand-footnote");
18+
expandableFootnote.innerHTML = iconHTML("ellipsis-h");
19+
expandableFootnote.href = "";
20+
expandableFootnote.role = "button";
21+
expandableFootnote.dataset.footnoteId = refLink.getAttribute("href");
22+
23+
footnoteRef.after(expandableFootnote);
24+
});
25+
26+
if (footnoteRefs.length) {
27+
elem.classList.add("inline-footnotes");
28+
}
29+
}
30+
31+
function buildTooltip() {
32+
let html = `
33+
<div id="footnote-tooltip" role="tooltip">
34+
<div class="footnote-tooltip-content"></div>
35+
<div id="arrow" data-popper-arrow></div>
36+
</div>
37+
`;
38+
39+
let template = document.createElement("template");
40+
html = html.trim();
41+
template.innerHTML = html;
42+
return template.content.firstChild;
43+
}
44+
45+
function footNoteEventHandler(event) {
46+
inlineFootnotePopper?.destroy();
47+
48+
const tooltip = document.getElementById("footnote-tooltip");
49+
50+
// reset state by hidding tooltip, it handles "click outside"
51+
// allowing to hide the tooltip when you click anywhere else
52+
tooltip?.removeAttribute("data-show");
53+
54+
// if we didn't actually click a footnote button, exit early
55+
if (!event.target.classList.contains("expand-footnote")) {
56+
return;
57+
}
58+
59+
event.preventDefault();
60+
event.stopPropagation();
61+
62+
// append footnote to tooltip body
63+
const expandableFootnote = event.target;
64+
const cooked = expandableFootnote.closest(".cooked");
65+
const footnoteId = expandableFootnote.dataset.footnoteId;
66+
const footnoteContent = tooltip.querySelector(".footnote-tooltip-content");
67+
let newContent = cooked.querySelector(footnoteId);
68+
69+
footnoteContent.innerHTML = newContent.innerHTML;
70+
71+
// display tooltip
72+
tooltip.dataset.show = "";
73+
74+
// setup popper
75+
inlineFootnotePopper?.destroy();
76+
inlineFootnotePopper = createPopper(expandableFootnote, tooltip, {
77+
modifiers: [
78+
{
79+
name: "arrow",
80+
options: { element: tooltip.querySelector("#arrow") },
81+
},
82+
{
83+
name: "preventOverflow",
84+
options: {
85+
altAxis: true,
86+
padding: 5,
87+
},
88+
},
89+
{
90+
name: "offset",
91+
options: {
92+
offset: [0, 12],
93+
},
94+
},
95+
],
96+
});
97+
}
98+
99+
export default {
100+
name: "inline-footnotes",
101+
102+
initialize(container) {
103+
if (!container.lookup("site-settings:main").display_footnotes_inline) {
104+
return;
105+
}
106+
107+
document.documentElement.append(buildTooltip());
108+
109+
window.addEventListener("click", footNoteEventHandler);
110+
111+
withPluginApi("0.8.9", (api) => {
112+
api.decorateCookedElement((elem) => applyInlineFootnotes(elem), {
113+
onlyStream: true,
114+
id: "inline-footnotes",
115+
});
116+
117+
api.onPageChange(() => {
118+
document
119+
.getElementById("footnote-tooltip")
120+
?.removeAttribute("data-show");
121+
});
122+
});
123+
},
124+
125+
teardown() {
126+
inlineFootnotePopper?.destroy();
127+
window.removeEventListener("click", footNoteEventHandler);
128+
document.getElementById("footnote-tooltip")?.remove();
129+
},
130+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export function setup(helper) {
2+
helper.registerOptions((opts, siteSettings) => {
3+
opts.features["footnotes"] =
4+
window.markdownitFootnote && !!siteSettings.enable_markdown_footnotes;
5+
});
6+
7+
helper.allowList([
8+
"ol.footnotes-list",
9+
"hr.footnotes-sep",
10+
"li.footnote-item",
11+
"a.footnote-backref",
12+
"sup.footnote-ref",
13+
]);
14+
15+
helper.allowList({
16+
custom(tag, name, value) {
17+
if ((tag === "a" || tag === "li") && name === "id") {
18+
return !!value.match(/^fn(ref)?\d+$/);
19+
}
20+
},
21+
});
22+
23+
if (window.markdownitFootnote) {
24+
helper.registerPlugin(window.markdownitFootnote);
25+
}
26+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
.inline-footnotes {
2+
a.expand-footnote {
3+
user-select: none;
4+
padding: 0px 0.5em;
5+
margin: 0 0 0 0.25em;
6+
color: var(--primary-low-mid-or-secondary-high);
7+
background: var(--primary-low);
8+
border-radius: 3px;
9+
min-height: 20px;
10+
display: inline-flex;
11+
align-items: center;
12+
13+
&:hover {
14+
background: var(--primary-medium);
15+
color: var(--secondary);
16+
}
17+
18+
> * {
19+
pointer-events: none;
20+
}
21+
}
22+
23+
// This is hack to work with lazy-loading, we will trick the browser
24+
// to believe the image is in the DOM and can be loaded
25+
.footnotes-list,
26+
.footnotes-sep {
27+
position: absolute;
28+
// the left/right positioning prevents overflow issues
29+
// with long words causing overflow on small screens
30+
left: 0;
31+
right: 0;
32+
}
33+
34+
.footnotes-sep,
35+
.footnotes-list,
36+
.footnote-ref {
37+
display: none;
38+
}
39+
}
40+
41+
#footnote-tooltip {
42+
background-color: var(--primary-low);
43+
color: var(--primary);
44+
padding: 0.5em;
45+
font-size: var(--font-down-1);
46+
border-radius: 3px;
47+
display: none;
48+
z-index: z("tooltip");
49+
max-width: 400px;
50+
overflow-wrap: break-word;
51+
52+
.footnote-tooltip-content {
53+
overflow: hidden;
54+
55+
.footnote-backref {
56+
display: none;
57+
}
58+
59+
img {
60+
object-fit: cover;
61+
max-width: 385px;
62+
}
63+
64+
p {
65+
margin: 0;
66+
}
67+
}
68+
}
69+
70+
#footnote-tooltip[data-show] {
71+
display: block;
72+
}
73+
74+
#arrow,
75+
#arrow::before {
76+
position: absolute;
77+
width: 10px;
78+
height: 10px;
79+
background: inherit;
80+
}
81+
82+
#arrow {
83+
visibility: hidden;
84+
}
85+
86+
#arrow::before {
87+
visibility: visible;
88+
content: "";
89+
transform: rotate(45deg);
90+
}
91+
92+
#footnote-tooltip[data-popper-placement^="top"] > #arrow {
93+
bottom: -4px;
94+
}
95+
96+
#footnote-tooltip[data-popper-placement^="bottom"] > #arrow {
97+
top: -4px;
98+
}
99+
100+
#footnote-tooltip[data-popper-placement^="left"] > #arrow {
101+
right: -4px;
102+
}
103+
104+
#footnote-tooltip[data-popper-placement^="right"] > #arrow {
105+
left: -4px;
106+
}

0 commit comments

Comments
 (0)