2022-01-18 09:18:21 -05:00
|
|
|
import domUtils from "discourse-common/utils/dom-utils";
|
|
|
|
import { headerOffset } from "discourse/lib/offset-calculator";
|
|
|
|
import { iconHTML } from "discourse-common/lib/icon-library";
|
2022-01-19 08:45:58 -05:00
|
|
|
import { later } from "@ember/runloop";
|
2022-01-18 09:18:21 -05:00
|
|
|
import { slugify } from "discourse/lib/utilities";
|
|
|
|
import { withPluginApi } from "discourse/lib/plugin-api";
|
|
|
|
import I18n from "I18n";
|
|
|
|
|
|
|
|
export default {
|
|
|
|
name: "disco-toc-main",
|
|
|
|
|
|
|
|
initialize() {
|
|
|
|
withPluginApi("1.0.0", (api) => {
|
|
|
|
api.decorateCookedElement(
|
|
|
|
(el, helper) => {
|
|
|
|
if (helper) {
|
|
|
|
const post = helper.getModel();
|
|
|
|
if (post.post_number !== 1) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!el.querySelector(`[data-theme-toc="true"]`)) {
|
2022-01-19 08:45:58 -05:00
|
|
|
document.body.classList.remove("d-toc-timeline-visible");
|
2022-01-18 09:18:21 -05:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let dTocHeadingSelectors =
|
2022-01-20 16:14:37 -05:00
|
|
|
":scope > h1, :scope > h2, :scope > h3, :scope > h4, :scope > h5";
|
2022-01-18 09:18:21 -05:00
|
|
|
const headings = el.querySelectorAll(dTocHeadingSelectors);
|
|
|
|
|
|
|
|
if (headings.length < 1) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
headings.forEach((h) => {
|
|
|
|
const id =
|
|
|
|
h.getAttribute("id") ||
|
|
|
|
slugify(`toc-${h.nodeName}-${h.textContent}`);
|
|
|
|
|
|
|
|
h.setAttribute("id", id);
|
|
|
|
h.setAttribute("data-d-toc", id);
|
|
|
|
h.classList.add("d-toc-post-heading");
|
|
|
|
});
|
|
|
|
|
|
|
|
el.classList.add("d-toc-cooked");
|
|
|
|
|
2022-01-19 08:45:58 -05:00
|
|
|
if (document.querySelector(".d-toc-wrapper")) {
|
|
|
|
this.insertTOC(headings);
|
2022-01-18 09:18:21 -05:00
|
|
|
} else {
|
2022-01-19 08:45:58 -05:00
|
|
|
// try again if decoration happens while outlet is not rendered
|
|
|
|
// this is due to core resetting `canRender` for topic-navigation
|
|
|
|
// when transitioning between topics
|
|
|
|
later(() => {
|
|
|
|
if (document.querySelector(".d-toc-wrapper")) {
|
|
|
|
this.insertTOC(headings);
|
|
|
|
}
|
|
|
|
}, 300);
|
2022-01-18 09:18:21 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
id: "disco-toc",
|
|
|
|
onlyStream: true,
|
|
|
|
afterAdopt: true,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
api.onAppEvent("topic:current-post-changed", (args) => {
|
|
|
|
if (!document.querySelector(".d-toc-cooked")) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (args.post.post_number === 1) {
|
|
|
|
document.body.classList.add("d-toc-timeline-visible");
|
|
|
|
} else {
|
|
|
|
document.body.classList.remove("d-toc-timeline-visible");
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2022-01-19 12:50:45 -05:00
|
|
|
api.onAppEvent("docs-topic:current-post-scrolled", () => {
|
|
|
|
this.updateTOCSidebar();
|
|
|
|
});
|
|
|
|
|
2022-01-18 09:18:21 -05:00
|
|
|
api.onAppEvent("topic:current-post-scrolled", (args) => {
|
|
|
|
if (args.postIndex !== 1) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-01-19 12:50:45 -05:00
|
|
|
this.updateTOCSidebar();
|
2022-01-18 09:18:21 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
api.cleanupStream(() => {
|
|
|
|
document.body.classList.remove("d-toc-timeline-visible");
|
|
|
|
document.removeEventListener("click", this.clickTOC, false);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2022-01-19 12:50:45 -05:00
|
|
|
updateTOCSidebar() {
|
|
|
|
if (!document.querySelector(".d-toc-cooked")) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const headings = document.querySelectorAll(".d-toc-post-heading");
|
|
|
|
let closestHeadingDistance = null;
|
|
|
|
let closestHeading = null;
|
|
|
|
|
|
|
|
headings.forEach((heading) => {
|
|
|
|
const distance = Math.abs(
|
|
|
|
domUtils.offset(heading).top - headerOffset() - window.scrollY
|
|
|
|
);
|
|
|
|
if (closestHeadingDistance == null || distance < closestHeadingDistance) {
|
|
|
|
closestHeadingDistance = distance;
|
|
|
|
closestHeading = heading;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (closestHeading) {
|
|
|
|
document.querySelectorAll("#d-toc li").forEach((listItem) => {
|
|
|
|
listItem.classList.remove("active");
|
|
|
|
listItem.classList.remove("direct-active");
|
|
|
|
});
|
|
|
|
|
|
|
|
const anchor = document.querySelector(
|
|
|
|
`#d-toc a[data-d-toc="${closestHeading.getAttribute("id")}"]`
|
|
|
|
);
|
|
|
|
|
|
|
|
if (!anchor) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
anchor.parentElement.classList.add("direct-active");
|
|
|
|
parentsUntil(anchor, "#d-toc", ".d-toc-item").forEach((liParent) => {
|
|
|
|
liParent.classList.add("active");
|
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2022-01-19 08:45:58 -05:00
|
|
|
insertTOC(headings) {
|
|
|
|
const dToc = document.createElement("div");
|
|
|
|
dToc.classList.add("d-toc-main");
|
|
|
|
dToc.innerHTML = `<div class="d-toc-icons">
|
|
|
|
<a href="" class="scroll-to-bottom" title="${I18n.t(
|
|
|
|
themePrefix("post_bottom_tooltip")
|
|
|
|
)}">${iconHTML("downward")}</a>
|
|
|
|
<a href="" class="d-toc-close">${iconHTML("times")}</a></div>`;
|
|
|
|
|
|
|
|
const existing = document.querySelector(".d-toc-wrapper .d-toc-main");
|
|
|
|
if (existing) {
|
|
|
|
document.querySelector(".d-toc-wrapper").replaceChild(dToc, existing);
|
|
|
|
} else {
|
|
|
|
document.querySelector(".d-toc-wrapper").appendChild(dToc);
|
|
|
|
}
|
|
|
|
|
2022-01-19 17:17:30 -05:00
|
|
|
const result = this.buildTOC(Array.from(headings));
|
2022-01-19 08:45:58 -05:00
|
|
|
document.querySelector(".d-toc-main").appendChild(result);
|
|
|
|
document.addEventListener("click", this.clickTOC, false);
|
|
|
|
},
|
|
|
|
|
2022-01-18 09:18:21 -05:00
|
|
|
clickTOC(e) {
|
|
|
|
if (!document.body.classList.contains("d-toc-timeline-visible")) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// link to each heading
|
2022-01-19 17:17:30 -05:00
|
|
|
if (
|
|
|
|
e.target.closest(".d-toc-item") &&
|
|
|
|
e.target.hasAttribute("data-d-toc")
|
|
|
|
) {
|
2022-01-18 09:18:21 -05:00
|
|
|
const target = `#${e.target.getAttribute("data-d-toc")}`;
|
|
|
|
const scrollTo = domUtils.offset(
|
|
|
|
document.querySelector(`.d-toc-cooked ${target}`)
|
|
|
|
).top;
|
|
|
|
window.scrollTo({
|
|
|
|
top: scrollTo - headerOffset() - 10,
|
|
|
|
behavior: "smooth",
|
|
|
|
});
|
|
|
|
document.querySelector(".d-toc-wrapper").classList.remove("overlay");
|
|
|
|
e.preventDefault();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (e.target.closest("a")) {
|
|
|
|
// link to first post bottom
|
|
|
|
if (e.target.closest("a").classList.contains("scroll-to-bottom")) {
|
|
|
|
const rect = document
|
|
|
|
.querySelector(".d-toc-cooked")
|
|
|
|
.getBoundingClientRect();
|
|
|
|
|
|
|
|
if (rect) {
|
|
|
|
window.scrollTo({
|
|
|
|
top: rect.bottom + window.scrollY - headerOffset() - 10,
|
|
|
|
behavior: "smooth",
|
|
|
|
});
|
2022-01-20 16:14:37 -05:00
|
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
return false;
|
2022-01-18 09:18:21 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// close overlay
|
|
|
|
if (e.target.closest("a").classList.contains("d-toc-close")) {
|
|
|
|
document.querySelector(".d-toc-wrapper").classList.remove("overlay");
|
2022-01-20 16:14:37 -05:00
|
|
|
e.preventDefault();
|
|
|
|
return false;
|
2022-01-18 09:18:21 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!document.querySelector(".d-toc-wrapper.overlay")) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// clicking outside overlay
|
|
|
|
if (!e.target.closest(".d-toc-wrapper.overlay")) {
|
|
|
|
document.querySelector(".d-toc-wrapper").classList.remove("overlay");
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2022-01-19 17:17:30 -05:00
|
|
|
buildTOC(headings) {
|
|
|
|
const result = document.createElement("div");
|
|
|
|
result.setAttribute("id", "d-toc");
|
|
|
|
|
|
|
|
const primaryH = headings[0].tagName;
|
|
|
|
const primaryHeadings = headings.filter((n) => n.tagName === primaryH);
|
|
|
|
let nextIndex = headings.length;
|
|
|
|
|
|
|
|
primaryHeadings.forEach((primaryHeading, index) => {
|
|
|
|
const ul = document.createElement("ul");
|
|
|
|
ul.classList.add("d-toc-heading");
|
|
|
|
|
|
|
|
let li = this.buildItem(primaryHeading);
|
|
|
|
ul.appendChild(li);
|
|
|
|
|
|
|
|
const currentIndex = headings.indexOf(primaryHeading);
|
|
|
|
if (primaryHeadings[index + 1]) {
|
|
|
|
nextIndex = headings.indexOf(primaryHeadings[index + 1]);
|
|
|
|
} else {
|
|
|
|
nextIndex = headings.length;
|
|
|
|
}
|
|
|
|
|
|
|
|
headings.forEach((heading, subIndex) => {
|
|
|
|
if (subIndex > currentIndex && subIndex < nextIndex) {
|
|
|
|
let subUl = li.lastChild;
|
2022-01-20 10:53:40 -05:00
|
|
|
if (subUl.tagName !== "UL") {
|
2022-01-19 17:17:30 -05:00
|
|
|
subUl = subUl.appendChild(document.createElement("ul"));
|
|
|
|
subUl.classList.add("d-toc-sublevel");
|
|
|
|
li.appendChild(subUl);
|
|
|
|
}
|
|
|
|
|
|
|
|
let subLi = this.buildItem(heading);
|
|
|
|
subUl.appendChild(subLi);
|
2022-01-18 09:18:21 -05:00
|
|
|
}
|
2022-01-19 17:17:30 -05:00
|
|
|
});
|
2022-01-18 09:18:21 -05:00
|
|
|
|
2022-01-19 17:17:30 -05:00
|
|
|
result.appendChild(ul);
|
|
|
|
});
|
|
|
|
|
|
|
|
return result;
|
|
|
|
},
|
2022-01-19 11:53:13 -05:00
|
|
|
|
2022-01-19 17:17:30 -05:00
|
|
|
buildItem(node) {
|
2022-01-19 11:53:13 -05:00
|
|
|
let clonedNode = node.cloneNode(true);
|
2022-01-20 10:53:40 -05:00
|
|
|
|
2022-01-19 11:53:13 -05:00
|
|
|
clonedNode.querySelector("span.clicks")?.remove();
|
2022-01-19 17:17:30 -05:00
|
|
|
const li = document.createElement("li");
|
|
|
|
li.classList.add("d-toc-item");
|
2022-01-20 16:14:37 -05:00
|
|
|
li.classList.add(`d-toc-${clonedNode.tagName.toLowerCase()}`);
|
2022-01-19 11:53:13 -05:00
|
|
|
|
|
|
|
li.innerHTML = `<a data-d-toc="${clonedNode.getAttribute("id")}">${
|
|
|
|
clonedNode.textContent
|
2022-01-18 09:18:21 -05:00
|
|
|
}</a>`;
|
|
|
|
|
2022-01-19 11:53:13 -05:00
|
|
|
clonedNode.remove();
|
2022-01-19 17:17:30 -05:00
|
|
|
return li;
|
|
|
|
},
|
|
|
|
};
|
2022-01-18 09:18:21 -05:00
|
|
|
|
|
|
|
function parentsUntil(el, selector, filter) {
|
|
|
|
const result = [];
|
|
|
|
const matchesSelector =
|
|
|
|
el.matches ||
|
|
|
|
el.webkitMatchesSelector ||
|
|
|
|
el.mozMatchesSelector ||
|
|
|
|
el.msMatchesSelector;
|
|
|
|
|
|
|
|
// match start from parent
|
|
|
|
el = el.parentElement;
|
|
|
|
while (el && !matchesSelector.call(el, selector)) {
|
|
|
|
if (!filter) {
|
|
|
|
result.push(el);
|
|
|
|
} else {
|
|
|
|
if (matchesSelector.call(el, filter)) {
|
|
|
|
result.push(el);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
el = el.parentElement;
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|