Skip to content

Instantly share code, notes, and snippets.

@B-iggy
Created May 22, 2019 13:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save B-iggy/6c2ef4807b0c7e923a245a512ce376b8 to your computer and use it in GitHub Desktop.
Save B-iggy/6c2ef4807b0c7e923a245a512ce376b8 to your computer and use it in GitHub Desktop.
Discourse TOC Anchor Links - Copy to Clipboard
<script type="text/discourse-plugin" version="0.1">
const computed = require("ember-addons/ember-computed-decorators").default;
const minimumOffset = require("discourse/lib/offset-calculator").minimumOffset;
const { iconHTML } = require("discourse-common/lib/icon-library");
const { run } = Ember;
const mobileView = $("html").hasClass("mobile-view");
const isSafari = !!navigator.userAgent.match(/Version\/[\d\.]+.*Safari/);
const scrollElemnt = isSafari ? "body" : "html";
const linkIcon = iconHTML("hashtag");
const closeIcon = iconHTML("times");
const dtocIcon = iconHTML("align-left");
const currUser = api.getCurrentUser();
const currUserTrustLevel = currUser ? currUser.trust_level : "";
const minimumTrustLevel = settings.minimum_trust_level_to_create_TOC;
const SCROLL_THROTTLE = 300;
const SMOOTH_SCROLL_SPEED = 300;
const TOC_ANIMATION_SPEED = 300;
const cleanUp = item => {
const cleanItem = item
.trim()
.toLowerCase()
.replace(/ /g, "-")
.replace(/\-\-+/g, "-")
.replace(/^\-/, "")
.replace(/\-$/, "");
return cleanItem;
};
const createAnchors = id => {
const link = $("<a/>", {
href: `#${id}`,
class: "d-toc-anchor-link",
html: linkIcon
});
return link;
};
const copyToClipboard = str => {
const el = document.createElement('textarea');
el.value = str;
document.body.appendChild(el);
el.select();
document.execCommand('copy');
document.body.removeChild(el);
};
const setUpTocItem = function(item) {
const unique = item.attr("id");
const text = item.text();
const tocItem = $("<li/>", {
class: "d-toc-item",
"data-d-toc": unique
});
tocItem.append(
$("<a/>", {
text: text
})
);
return tocItem;
};
(function(dToc) {
dToc($, window);
$.widget("discourse.dToc", {
_create: function() {
this.generateDtoc();
this.setEventHandlers();
},
generateDtoc: function() {
const self = this;
const primaryHeadings = $(this.options.cooked).find(
this.options.selectors.substr(0, this.options.selectors.indexOf(","))
);
self.element.addClass("d-toc");
primaryHeadings.each(function(index) {
const selectors = self.options.selectors,
ul = $("<ul/>", {
id: `d-toc-top-heading-${index}`,
class: "d-toc-heading"
});
ul.append(setUpTocItem($(this)));
self.element.append(ul);
$(this)
.nextUntil(this.nodeName.toLowerCase())
.each(function() {
const headings = $(this).find(selectors).length
? $(this).find(selectors)
: $(this).filter(selectors);
headings.each(function() {
self.nestTocItem.call(this, self, ul);
});
});
});
},
nestTocItem: function(self, ul) {
const index = $(this).index(self.options.selectors);
const previousHeader = $(self.options.selectors).eq(index - 1);
const previousTagName = previousHeader.prop("tagName").charAt(1);
const currentTagName = $(this)
.prop("tagName")
.charAt(1);
if (currentTagName < previousTagName) {
self.element
.find(`.d-toc-subheading[data-tag="${currentTagName}"]`)
.last()
.append(setUpTocItem($(this)));
} else if (currentTagName === previousTagName) {
ul.find(".d-toc-item")
.last()
.after(setUpTocItem($(this)));
} else {
ul.find(".d-toc-item")
.last()
.after(
$("<ul/>", {
class: "d-toc-subheading",
"data-tag": currentTagName
})
)
.next(".d-toc-subheading")
.append(setUpTocItem($(this)));
}
},
setEventHandlers: function() {
const self = this;
const dtocMobile = () => {
$(".d-toc").toggleClass("d-toc-mobile");
};
this.element.on("click.d-toc", "li", function() {
self.element.find(".d-toc-active").removeClass("d-toc-active");
$(this).addClass("d-toc-active");
if (mobileView) {
dtocMobile();
} else {
let elem = $(`li[data-d-toc="${$(this).attr("data-d-toc")}"]`);
self.triggerShowHide(elem);
}
self.scrollTo($(this));
});
$("#main").on(
"click.toggleDtoc",
".d-toc-toggle, .d-toc-close",
dtocMobile
);
const onScroll = () => {
run.throttle(this, self.highlightItemsOnScroll, self, SCROLL_THROTTLE);
};
$(window).on("scroll.d-toc", onScroll);
},
highlightItemsOnScroll: self => {
$(scrollElemnt)
.promise()
.done(function() {
const winScrollTop = $(window).scrollTop();
const anchors = $(self.options.cooked).find("[data-d-toc]");
let closestAnchorDistance = null;
let closestAnchorIdx = null;
anchors.each(function(idx) {
const distance = Math.abs(
$(this).offset().top - minimumOffset() - winScrollTop
);
if (
closestAnchorDistance == null ||
distance < closestAnchorDistance
) {
closestAnchorDistance = distance;
closestAnchorIdx = idx;
} else {
return false;
}
});
const anchorText = $(anchors[closestAnchorIdx]).attr("data-d-toc");
const elem = $(`li[data-d-toc="${anchorText}"]`);
if (elem.length) {
self.element.find(".d-toc-active").removeClass("d-toc-active");
elem.addClass("d-toc-active");
}
if (!mobileView) {
self.triggerShowHide(elem);
}
});
},
triggerShowHide: function(elem) {
if (
elem.parent().is(".d-toc-heading") ||
elem.next().is(".d-toc-subheading")
) {
this.showHide(elem.next(".d-toc-subheading"));
} else if (elem.parent().is(".d-toc-subheading")) {
this.showHide(elem.parent());
}
},
showHide: function(elem) {
return elem.is(":visible") ? this.hide(elem) : this.show(elem);
},
hide: function(elem) {
const target = $(".d-toc-subheading")
.not(elem)
.not(elem.parent(".d-toc-subheading:has(.d-toc-active)"));
return isSafari
? target.fadeOut(TOC_ANIMATION_SPEED)
: target.slideUp(TOC_ANIMATION_SPEED);
},
show: function(elem) {
return isSafari
? elem.fadeIn(TOC_ANIMATION_SPEED)
: elem.slideDown(TOC_ANIMATION_SPEED);
},
scrollTo: function(elem) {
const currentDiv = $(`[data-d-toc="${elem.attr("data-d-toc")}"]`);
$(scrollElemnt).animate(
{
scrollTop: `${currentDiv.offset().top - minimumOffset()}`
},
{
duration: SMOOTH_SCROLL_SPEED
}
);
},
setOptions: () => {
$.Widget.prototype._setOptions.apply(this, arguments);
}
});
})(() => {});
api.decorateCooked($elem => {
run.scheduleOnce("actions", () => {
if ($elem.hasClass("d-editor-preview")) return;
if (!$elem.parents("article#post_1").length) return;
const dToc = $elem.find(`[data-theme-toc="true"]`);
if (!dToc.length) return this;
const body = $elem;
body.find("div, aside, blockquote, article, details").each(function() {
$(this)
.children("h1,h2,h3,h4,h5,h6")
.each(function() {
$(this).replaceWith(
`<div class="d-toc-ignore">${$(this).html()}</div>`
);
});
});
let dTocHeadingSelectors = "h1,h2,h3,h4,h5,h6";
if (!body.has(">h1").length) {
dTocHeadingSelectors = "h2,h3,h4,h5,h6";
if (!body.has(">h2").length) {
dTocHeadingSelectors = "h3,h4,h5,h6";
if (!body.has(">h3").length) {
dTocHeadingSelectors = "h4,h5,h6";
if (!body.has(">h4").length) {
dTocHeadingSelectors = "h5,h6";
if (!body.has(">h5").length) {
dTocHeadingSelectors = "h6";
}
}
}
}
}
body.find(dTocHeadingSelectors).each(function() {
if ($(this).hasClass("d-toc-ignore")) return;
const heading = $(this);
let id = heading.attr("id") || "";
if (!id.length) {
id = cleanUp(heading.text());
}
heading
.attr({
id: id,
"data-d-toc": id
})
.append(createAnchors(id))
.addClass("d-toc-post-heading");
var anchorLink = $(this).find('a').attr('href'),
fullUrl = window.location.hostname + window.location.pathname + anchorLink;
heading.on("click", function(e) {
copyToClipboard( fullUrl );
});
});
body
.addClass("d-toc-cooked")
.prepend(
`<span class="d-toc-toggle">
${dtocIcon} ${I18n.t(themePrefix("table_of_contents"))}
</span>`
)
.parents(".regular")
.addClass("d-toc-regular")
.parents("article")
.addClass("d-toc-article")
.append(
`<ul id="d-toc">
<div class="d-toc-close-wrapper">
<div class="d-toc-close">
${closeIcon}
</div>
</div>
</ul>`
)
.parents(".topic-post")
.addClass("d-toc-post")
.parents("body")
.addClass("d-toc-timeline");
$("#d-toc").dToc({
cooked: body,
selectors: dTocHeadingSelectors
});
});
});
api.cleanupStream(() => {
$(window).off("scroll.d-toc");
$("#main").off("click.toggleDtoc");
$(".d-toc-timeline").removeClass("d-toc-timeline d-toc-timeline-visible");
});
api.onAppEvent("topic:current-post-changed", post => {
if (!$(".d-toc-timeline").length) return;
run.scheduleOnce("afterRender", () => {
if (post.post.post_number <= 2) {
$("body").removeClass("d-toc-timeline-visible");
$(".d-toc-toggle").fadeIn(100);
} else {
$("body").addClass("d-toc-timeline-visible");
$(".d-toc-toggle").fadeOut(100);
}
});
});
if (currUserTrustLevel >= minimumTrustLevel) {
if (!I18n.translations[I18n.currentLocale()].js.composer) {
I18n.translations[I18n.currentLocale()].js.composer = {};
}
I18n.translations[I18n.currentLocale()].js.composer.contains_dtoc = " ";
api.addToolbarPopupMenuOptionsCallback(() => {
const composerController = api.container.lookup("controller:composer");
return {
action: "insertDtoc",
icon: "align-left",
label: themePrefix("insert_table_of_contents"),
condition: composerController.get("model.canCategorize")
};
});
api.modifyClass("controller:composer", {
actions: {
insertDtoc() {
this.get("toolbarEvent").applySurround(
`<div data-theme-toc="true">`,
`</div>`,
"contains_dtoc"
);
}
}
});
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment