DiscoTOC v0.2

This commit is contained in:
Joe 2019-04-14 13:47:22 -07:00 committed by GitHub
parent 94cb1f1a4b
commit 1ee43956c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 420 additions and 345 deletions

View file

@ -1,23 +1,6 @@
@import "common/foundation/variables"; @import "common/foundation/variables";
.d-toc-regular { .d-toc-regular {
.cooked {
h1,
h2,
h3,
h4,
h5,
h6 {
&:before {
display: block;
content: "";
height: 70px;
margin-top: -70px;
pointer-events: none;
opacity: 0;
}
}
}
[data-theme-toc] { [data-theme-toc] {
display: none; display: none;
} }
@ -38,9 +21,9 @@
margin: 0; margin: 0;
padding: 0; padding: 0;
border: none; border: none;
line-height: 30px;
} }
.d-toc-item { .d-toc-item {
padding: 6px 0;
a { a {
color: $primary-high; color: $primary-high;
} }
@ -149,9 +132,11 @@
font-size: 0.8em; font-size: 0.8em;
} }
} }
.d-toc-post-heading {
.d-toc-anchor-link { .d-toc-anchor-link {
font-size: initial; font-size: initial;
color: $primary-medium; color: transparent;
transition: color 0.15s linear;
:not(.rtl) & { :not(.rtl) & {
margin-left: 5px; margin-left: 5px;
} }
@ -159,6 +144,10 @@
margin-right: 5px; margin-right: 5px;
} }
} }
&:hover .d-toc-anchor-link {
color: rgba($primary-medium, 0.6);
}
}
} }
// large screens // large screens
@ -179,7 +168,7 @@
max-height: 85vh; max-height: 85vh;
padding-left: 0; padding-left: 0;
margin-top: 50px; margin-top: 50px;
position: -webkit-sticky;
position: sticky; position: sticky;
top: 75px; top: 75px;
margin-bottom: 135px; margin-bottom: 135px;
@ -197,6 +186,12 @@
} }
.d-toc-article { .d-toc-article {
display: flex; display: flex;
.post-notice {
display: none;
}
.topic-map {
margin-bottom: 0;
}
> .row { > .row {
:not(.rtl) & { :not(.rtl) & {
border-right: 1px solid $primary-low; border-right: 1px solid $primary-low;
@ -260,6 +255,7 @@
background: $secondary; background: $secondary;
margin-bottom: 1em; margin-bottom: 1em;
position: -webkit-sticky;
position: sticky; position: sticky;
top: 0; top: 0;
display: flex; display: flex;
@ -303,14 +299,33 @@
} }
} }
.d-toc-timeline {
.timeline-container,
#topic-progress-wrapper {
opacity: 0;
pointer-events: none;
}
&.d-toc-timeline-visible {
.timeline-container,
#topic-progress-wrapper {
opacity: 1;
pointer-events: initial;
}
}
}
.edit-title .d-editor-preview [data-theme-toc] { .edit-title .d-editor-preview [data-theme-toc] {
background: $tertiary; background: $tertiary;
color: $secondary; color: $secondary;
border-top: 2px solid $secondary; border-top: 2px solid $secondary;
position: -webkit-sticky;
position: sticky; position: sticky;
top: 0; top: 0;
height: 30px; height: 30px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
&:before {
content: "#{$composer_toc_text}";
}
} }

View file

@ -1,34 +1,66 @@
<script type="text/discourse-plugin" version="0.1"> <script type="text/discourse-plugin" version="0.1">
const mobileView = $("html").hasClass("mobile-view"), const computed = require("ember-addons/ember-computed-decorators").default;
{ iconHTML } = require("discourse-common/lib/icon-library"), const minimumOffset = require("discourse/lib/offset-calculator").minimumOffset;
{ run } = Ember, const { iconHTML } = require("discourse-common/lib/icon-library");
linkIcon = iconHTML("link"), const { run } = Ember;
closeIcon = iconHTML("times"),
dtocIcon = iconHTML("align-left"), const mobileView = $("html").hasClass("mobile-view");
items = [], const isSafari = !!navigator.userAgent.match(/Version\/[\d\.]+.*Safari/);
cleanUp = item => { const scrollElemnt = isSafari ? "body" : "html";
return item
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() .trim()
.toLowerCase() .toLowerCase()
.replace(/[^\w]+/g, "-") .replace(/[^\w]+/g, "-")
.replace(/^\-/, "") .replace(/^\-/, "")
.replace(/\-$/, "") .replace(/\-$/, "")
.replace(/\-\-+/g, "-"); .replace(/\-\-+/g, "-");
},
createAnchors = id => { return cleanItem;
let link = $("<a/>", { };
const createAnchors = id => {
const link = $("<a/>", {
href: `#${id}`, href: `#${id}`,
class: "d-toc-anchor-link", class: "d-toc-anchor-link",
html: linkIcon html: linkIcon
}); });
return link; return link;
},
dtocMobile = () => {
$(".d-toc").toggleClass("d-toc-mobile");
}; };
I18n.translations[I18n.currentLocale()].js.composer.contains_dtoc = I18n.t(
themePrefix("topic_will_contain_a_table_of_contents") const setUpTocItem = function(item) {
const unique = item.attr("id");
const text = item.text();
item.attr({ "data-d-toc": unique });
const tocItem = $("<li/>", {
class: "d-toc-item",
"data-d-toc": unique
});
tocItem.append(
$("<a/>", {
text: text
})
); );
return tocItem;
};
(function(dToc) { (function(dToc) {
dToc($, window); dToc($, window);
$.widget("discourse.dToc", { $.widget("discourse.dToc", {
@ -36,67 +68,58 @@ I18n.translations[I18n.currentLocale()].js.composer.contains_dtoc = I18n.t(
this.generateDtoc(); this.generateDtoc();
this.setEventHandlers(); this.setEventHandlers();
}, },
generateDtoc: function() { generateDtoc: function() {
let self = this, const self = this;
primaryHeadings = $(this.options.cooked).find(
const primaryHeadings = $(this.options.cooked).find(
this.options.selectors.substr(0, this.options.selectors.indexOf(",")) this.options.selectors.substr(0, this.options.selectors.indexOf(","))
); );
self.element.addClass("d-toc"); self.element.addClass("d-toc");
primaryHeadings.each(function(index) { primaryHeadings.each(function(index) {
let selectors = self.options.selectors, const selectors = self.options.selectors,
ul = $("<ul/>", { ul = $("<ul/>", {
id: `d-toc-top-heading-${index}`, id: `d-toc-top-heading-${index}`,
class: "d-toc-heading" class: "d-toc-heading"
}); });
ul.append(self.nestElements($(this), index));
ul.append(setUpTocItem($(this)));
self.element.append(ul); self.element.append(ul);
$(this) $(this)
.nextUntil(this.nodeName.toLowerCase()) .nextUntil(this.nodeName.toLowerCase())
.each(function() { .each(function() {
headings = $(this).find(selectors).length const headings = $(this).find(selectors).length
? $(this).find(selectors) ? $(this).find(selectors)
: $(this).filter(selectors); : $(this).filter(selectors);
headings.each(function() { headings.each(function() {
self.subheadings.call(this, self, ul); self.nestTocItem.call(this, self, ul);
}); });
}); });
}); });
}, },
nestElements: function(self, index) {
let unique = self.attr("id"), nestTocItem: function(self, ul) {
text = self.text(), const index = $(this).index(self.options.selectors);
arr, const previousHeader = $(self.options.selectors).eq(index - 1);
item; const previousTagName = previousHeader.prop("tagName").charAt(1);
arr = $.grep(items, i => i === text);
items.push(text); const currentTagName = $(this)
item = $("<li/>", {
class: "d-toc-item",
"data-d-toc": unique
});
item.append(
$("<a/>", {
text: text
})
);
self.attr({ "data-d-toc": unique });
return item;
},
subheadings: function(self, ul) {
let index = $(this).index(self.options.selectors),
previousHeader = $(self.options.selectors).eq(index - 1),
currentTagName = +$(this)
.prop("tagName") .prop("tagName")
.charAt(1), .charAt(1);
previousTagName = +previousHeader.prop("tagName").charAt(1);
if (currentTagName < previousTagName) { if (currentTagName < previousTagName) {
self.element self.element
.find(`.d-toc-subheading[data-tag="${currentTagName}"]`) .find(`.d-toc-subheading[data-tag="${currentTagName}"]`)
.last() .last()
.append(self.nestElements($(this), index)); .append(setUpTocItem($(this)));
} else if (currentTagName === previousTagName) { } else if (currentTagName === previousTagName) {
ul.find(".d-toc-item") ul.find(".d-toc-item")
.last() .last()
.after(self.nestElements($(this), index)); .after(setUpTocItem($(this)));
} else { } else {
ul.find(".d-toc-item") ul.find(".d-toc-item")
.last() .last()
@ -107,39 +130,57 @@ I18n.translations[I18n.currentLocale()].js.composer.contains_dtoc = I18n.t(
}) })
) )
.next(".d-toc-subheading") .next(".d-toc-subheading")
.append(self.nestElements($(this), index)); .append(setUpTocItem($(this)));
} }
}, },
setEventHandlers: function() { setEventHandlers: function() {
let self = this, const self = this;
scrollThrottle = mobileView ? 10 : 50,
dTocTimeout; const dtocMobile = () => {
$(".d-toc").toggleClass("d-toc-mobile");
};
this.element.on("click.d-toc", "li", function() { this.element.on("click.d-toc", "li", function() {
self.element.find(".d-toc-active").removeClass("d-toc-active"); self.element.find(".d-toc-active").removeClass("d-toc-active");
$(this).addClass("d-toc-active"); $(this).addClass("d-toc-active");
if (!mobileView) {
let elem = $(`li[data-d-toc="${$(this).attr("data-d-toc")}"]`);
self.triggerShow(elem);
}
self.scrollTo($(this));
if (mobileView) { if (mobileView) {
$("#d-toc").removeClass("d-toc-mobile"); dtocMobile();
} else {
let elem = $(`li[data-d-toc="${$(this).attr("data-d-toc")}"]`);
self.triggerShowHide(elem);
} }
self.scrollTo($(this));
}); });
$(window).on("scroll.d-toc", function() {
if (!dTocTimeout) { $("#main").on(
dTocTimeout = setTimeout(function() { "click.toggleDtoc",
$("html") ".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() .promise()
.done(function() { .done(function() {
let winScrollTop = $(window).scrollTop(), const winScrollTop = $(window).scrollTop();
elem; const anchors = $(self.options.cooked).find("[data-d-toc]");
let closestAnchorDistance = null,
closestAnchorIdx = null, let closestAnchorDistance = null;
anchors = $(self.options.cooked).find("[data-d-toc]"), let closestAnchorIdx = null;
anchorText;
anchors.each(function(idx) { anchors.each(function(idx) {
let distance = Math.abs($(this).offset().top - winScrollTop); const distance = Math.abs(
$(this).offset().top - minimumOffset() - winScrollTop
);
if ( if (
closestAnchorDistance == null || closestAnchorDistance == null ||
distance < closestAnchorDistance distance < closestAnchorDistance
@ -150,88 +191,81 @@ I18n.translations[I18n.currentLocale()].js.composer.contains_dtoc = I18n.t(
return false; return false;
} }
}); });
anchorText = $(anchors[closestAnchorIdx]).attr("data-d-toc");
elem = $(`li[data-d-toc="${anchorText}"]`); const anchorText = $(anchors[closestAnchorIdx]).attr("data-d-toc");
const elem = $(`li[data-d-toc="${anchorText}"]`);
if (elem.length) { if (elem.length) {
self.element self.element.find(".d-toc-active").removeClass("d-toc-active");
.find(".d-toc-active")
.removeClass("d-toc-active");
elem.addClass("d-toc-active"); elem.addClass("d-toc-active");
} }
self.triggerShow(elem, true);
});
dTocTimeout = null;
}, scrollThrottle);
}
});
},
show: function(elem) {
if (!elem.is(":visible")) {
if (
!elem.find(".d-toc-subheading").length &&
!elem.parent().is(".d-toc-heading") &&
!elem.parent().is(":visible")
) {
elem = elem.parents(".d-toc-subheading").add(elem);
} else if (
!elem.children(".d-toc-subheading").length &&
!elem.parent().is(".d-toc-heading")
) {
elem = elem.closest(".d-toc-subheading");
}
elem.slideDown("medium");
}
if (!mobileView) { if (!mobileView) {
if (elem.parent().is(".d-toc-heading")) { self.triggerShowHide(elem);
$(".d-toc-subheading")
.not(elem)
.slideUp("medium");
$(".d-toc-subheading")
.not(
elem
.closest(".d-toc-heading")
.find(".d-toc-subheading")
.not(elem.siblings())
)
.slideUp("medium");
}
} }
});
}, },
triggerShow: function(elem, scroll) {
triggerShowHide: function(elem) {
if ( if (
elem.parent().is(".d-toc-heading") || elem.parent().is(".d-toc-heading") ||
elem.next().is(".d-toc-subheading") elem.next().is(".d-toc-subheading")
) { ) {
this.show(elem.next(".d-toc-subheading"), scroll); this.showHide(elem.next(".d-toc-subheading"));
} else if (elem.parent().is(".d-toc-subheading")) { } else if (elem.parent().is(".d-toc-subheading")) {
this.show(elem.parent(), scroll); this.showHide(elem.parent());
} }
}, },
setOptions: function() {
$.Widget.prototype._setOptions.apply(this, arguments); 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) { scrollTo: function(elem) {
let currentDiv = $(`[data-d-toc="${elem.attr("data-d-toc")}"]`); const currentDiv = $(`[data-d-toc="${elem.attr("data-d-toc")}"]`);
$("html").animate(
$(scrollElemnt).animate(
{ {
scrollTop: currentDiv.offset().top + "px" scrollTop: `${currentDiv.offset().top - minimumOffset()}`
}, },
{ {
duration: 750 duration: SMOOTH_SCROLL_SPEED
} }
); );
},
setOptions: () => {
$.Widget.prototype._setOptions.apply(this, arguments);
} }
}); });
})(() => {}); })(() => {});
$.fn.dtoc = $elem => {
run.scheduleOnce("sync", () => { api.decorateCooked($elem => {
run.scheduleOnce("afterRender", () => {
if ($elem.hasClass("d-editor-preview")) return; if ($elem.hasClass("d-editor-preview")) return;
if (!$elem.parents("article#post_1").length) return; if (!$elem.parents("article#post_1").length) return;
const dToc = $elem.find(`[data-theme-toc="true"]`); const dToc = $elem.find(`[data-theme-toc="true"]`);
if (!dToc.length) return this; if (!dToc.length) return this;
const body = $elem; const body = $elem;
body.find("div, aside, blockquote, article").each(function() { body.find("div, aside, blockquote, article, details").each(function() {
$(this) $(this)
.children("h1,h2,h3,h4,h5,h6") .children("h1,h2,h3,h4,h5,h6")
.each(function() { .each(function() {
@ -240,6 +274,7 @@ $.fn.dtoc = $elem => {
); );
}); });
}); });
let dTocHeadingSelectors = "h1,h2,h3,h4,h5,h6"; let dTocHeadingSelectors = "h1,h2,h3,h4,h5,h6";
if (!body.has(">h1").length) { if (!body.has(">h1").length) {
dTocHeadingSelectors = "h2,h3,h4,h5,h6"; dTocHeadingSelectors = "h2,h3,h4,h5,h6";
@ -256,19 +291,27 @@ $.fn.dtoc = $elem => {
} }
} }
} }
body.find(dTocHeadingSelectors).each(function() { body.find(dTocHeadingSelectors).each(function() {
if ($(this).hasClass("d-toc-ignore")) return; if ($(this).hasClass("d-toc-ignore")) return;
let heading = $(this), const heading = $(this);
id = heading.attr("id") || "";
let id = heading.attr("id") || "";
if (id.length) { if (id.length) {
heading.attr("id", id); heading
heading.append(createAnchors(id)); .attr("id", id)
return; .append(createAnchors(id))
} .addClass("d-toc-post-heading");
} else {
id = cleanUp(heading.text()); id = cleanUp(heading.text());
heading.attr("id", id); heading
heading.append(createAnchors(id)); .attr("id", id)
.append(createAnchors(id))
.addClass("d-toc-post-heading");
}
}); });
body body
.addClass("d-toc-cooked") .addClass("d-toc-cooked")
.prepend( .prepend(
@ -290,40 +333,41 @@ $.fn.dtoc = $elem => {
</ul>` </ul>`
) )
.parents(".topic-post") .parents(".topic-post")
.addClass("d-toc-post"); .addClass("d-toc-post")
.parents("body")
.addClass("d-toc-timeline");
$("#d-toc").dToc({ $("#d-toc").dToc({
cooked: body, cooked: body,
selectors: dTocHeadingSelectors selectors: dTocHeadingSelectors
}); });
$(".d-toc-post").on(
"click.toggleDtoc",
".d-toc-toggle, .d-toc-close",
dtocMobile
);
}); });
$(".selected-posts") });
.next()
.addClass("d-toc-timeline"); api.cleanupStream(() => {
};
api.decorateCooked($elem => $elem.dtoc($elem));
api.modifyClass("component:topic-timeline", {
willDestroyElement() {
$(window).off("scroll.d-toc"); $(window).off("scroll.d-toc");
$(".d-toc-post").off("click.toggleDtoc"); $("#main").off("click.toggleDtoc");
} $(".d-toc-timeline").removeClass("d-toc-timeline d-toc-timeline-visible");
}); });
api.onAppEvent("topic:current-post-changed", post => { api.onAppEvent("topic:current-post-changed", post => {
if (!$(".d-toc-regular").length) return; if (!$(".d-toc-timeline").length) return;
if (post.post.post_number === 1) { run.scheduleOnce("afterRender", () => {
$(".d-toc-timeline").hide(); if (post.post.post_number <= 2) {
$(".d-toc-toggle").fadeIn(); $("body").removeClass("d-toc-timeline-visible");
$(".d-toc-toggle").fadeIn(100);
} else { } else {
$(".d-toc-timeline").fadeIn(); $("body").addClass("d-toc-timeline-visible");
$(".d-toc-toggle").fadeOut(); $(".d-toc-toggle").fadeOut(100);
} }
}); });
});
if (currUserTrustLevel >= minimumTrustLevel) {
I18n.translations[I18n.currentLocale()].js.composer.contains_dtoc = "";
api.addToolbarPopupMenuOptionsCallback(() => { api.addToolbarPopupMenuOptionsCallback(() => {
composerController = api.container.lookup("controller:composer"); const composerController = api.container.lookup("controller:composer");
return { return {
action: "insertDtoc", action: "insertDtoc",
icon: "align-left", icon: "align-left",
@ -331,6 +375,7 @@ api.addToolbarPopupMenuOptionsCallback(() => {
condition: composerController.get("model.canCategorize") condition: composerController.get("model.canCategorize")
}; };
}); });
api.modifyClass("controller:composer", { api.modifyClass("controller:composer", {
actions: { actions: {
insertDtoc() { insertDtoc() {
@ -342,4 +387,5 @@ api.modifyClass("controller:composer", {
} }
} }
}); });
}
</script> </script>

View file

@ -1,4 +1,3 @@
en: en:
table_of_contents: "table of contents" table_of_contents: "table of contents"
insert_table_of_contents: "Insert table of contents" insert_table_of_contents: "Insert table of contents"
topic_will_contain_a_table_of_contents: "This topic will contain a table of contents"

View file

@ -1,2 +1,17 @@
minimum_trust_level_to_create_TOC:
default: 1
type: enum
choices:
- 0
- 1
- 2
- 3
- 4
description:
en: "The minimum trust level a user must have in order to see the TOC button in the composer"
composer_toc_text:
default: "This topic will contain a table of contents"
table_of_contents_icon: table_of_contents_icon:
default: "align-left" default: "align-left"
anchor_icon:
default: "hashtag"