+ content.includes(tag)
+ );
+ }
+
+ populateTocData(postId, content, headings) {
+ this.hasTOC = true;
+ this.postID = postId;
+ this.postContent = content;
+ this.tocStructure = this.generateTocStructure(headings);
+ }
+
+ autoTOC(topic) {
+ // check topic for categories or tags from settings
+ const autoCategories = settings.auto_TOC_categories
+ ? settings.auto_TOC_categories.split("|").map((id) => parseInt(id, 10))
+ : [];
+
+ const autoTags = settings.auto_TOC_tags
+ ? settings.auto_TOC_tags.split("|")
+ : [];
+
+ if ((!autoCategories.length && !autoTags.length) || !topic) {
+ return false;
+ }
+
+ const topicCategory = topic.category_id;
+ const topicTags = topic.tags || [];
+
+ const hasMatchingTags = autoTags.some((tag) => topicTags.includes(tag));
+ const hasMatchingCategory = autoCategories.includes(topicCategory);
+
+ // only apply autoTOC on first post
+ // the docs plugin only shows the first post, and does not have topic.currentPost defined
+ return (
+ (hasMatchingTags || hasMatchingCategory) &&
+ (topic.currentPost === 1 || topic.currentPost === undefined)
+ );
+ }
+
+ generateTocStructure(headings) {
+ let root = { subItems: [], level: 0 };
+ let ancestors = [root];
+
+ headings.forEach((heading, index) => {
+ const level = parseInt(heading.tagName[1], 10);
+ const text = heading.textContent.trim();
+ const lowerTagName = heading.tagName.toLowerCase();
+ const anchor = heading.querySelector("a.anchor");
+
+ let id;
+ if (anchor) {
+ id = anchor.name;
+ } else {
+ id = `toc-${lowerTagName}-${slugify(text) || index}`;
+ }
+
+ // Remove irrelevant ancestors
+ while (ancestors[ancestors.length - 1].level >= level) {
+ ancestors.pop();
+ }
+
+ let headingData = {
+ id,
+ tagName: lowerTagName,
+ text,
+ subItems: [],
+ level,
+ parent: ancestors.length > 1 ? ancestors[ancestors.length - 1] : null,
+ };
+
+ ancestors[ancestors.length - 1].subItems.push(headingData);
+ ancestors.push(headingData);
+ });
+
+ return root.subItems;
+ }
+
+ jumpToEnd(renderTimeline, postID) {
+ const buffer = 150;
+ const postContainer = document.querySelector(`[data-post-id="${postID}"]`);
+
+ if (!renderTimeline) {
+ this.setOverlayVisible(false);
+ }
+
+ if (postContainer) {
+ // if the topic map is present, we don't want to scroll past it
+ // so the post controls are still visible
+ const topicMapHeight =
+ postContainer.querySelector(`.topic-map`)?.offsetHeight || 0;
+
+ const offsetPosition =
+ postContainer.getBoundingClientRect().bottom +
+ window.scrollY -
+ buffer -
+ topicMapHeight;
+
+ window.scrollTo({ top: offsetPosition, behavior: "smooth" });
+ }
+ }
+}
diff --git a/locales/en.yml b/locales/en.yml
index 7b587ec..52db0e0 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -1,7 +1,10 @@
en:
table_of_contents: table of contents
insert_table_of_contents: Insert table of contents
- post_bottom_tooltip: Navigate to post controls
+ jump_bottom: Jump to end
+ toggle_toc:
+ show_timeline: Timeline
+ show_toc: Contents
theme_metadata:
settings:
minimum_trust_level_to_create_TOC: The minimum trust level a user must have in order to see the TOC button in the composer
diff --git a/spec/system/discotoc_author_spec.rb b/spec/system/discotoc_author_spec.rb
new file mode 100644
index 0000000..3fb034b
--- /dev/null
+++ b/spec/system/discotoc_author_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+RSpec.describe "DiscoTOC", system: true do
+ let!(:theme) { upload_theme_component }
+
+ fab!(:category)
+ fab!(:user) { Fabricate(:user, trust_level: TrustLevel[1], refresh_auto_groups: true) }
+
+ fab!(:topic_1) { Fabricate(:topic) }
+ fab!(:post_1) {
+ Fabricate(:post, raw: "\n\n# Heading 1\nContent for the first heading\n## Heading 2\nContent for the second heading\n### Heading 3\nContent for the third heading\n# Heading 4\nContent for the fourth heading", topic: topic_1)
+ }
+
+ before do
+ sign_in(user)
+ end
+
+ it "composer has table of contents button" do
+ visit("/c/#{category.id}")
+
+ find("#create-topic").click
+ find(".toolbar-popup-menu-options").click
+
+ expect(page).to have_css("[data-name='Insert table of contents']")
+ end
+
+ it "table of contents button inserts markup into composer" do
+ visit("/c/#{category.id}")
+
+ find("#create-topic").click
+ find(".toolbar-popup-menu-options").click
+ find("[data-name='Insert table of contents']").click
+
+ expect(page).to have_css(".d-editor-preview [data-theme-toc='true']")
+ end
+
+ it "table of contents button is hidden by trust level setting" do
+ theme.update_setting(:minimum_trust_level_to_create_TOC, "2" )
+ theme.save!
+
+ visit("/c/#{category.id}")
+
+ find("#create-topic").click
+ find(".toolbar-popup-menu-options").click
+
+ expect(page).to have_no_css("[data-name='Insert table of contents']")
+ end
+
+ it "table of contents button does not appear on replies" do
+ visit("/t/#{topic_1.id}")
+
+ find(".reply").click
+ find(".toolbar-popup-menu-options").click
+
+ expect(page).to have_no_css("[data-name='Insert table of contents']")
+ end
+
+end
diff --git a/spec/system/discotoc_progress_user_spec.rb b/spec/system/discotoc_progress_user_spec.rb
new file mode 100644
index 0000000..0aa63fd
--- /dev/null
+++ b/spec/system/discotoc_progress_user_spec.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+RSpec.describe "DiscoTOC", system: true do
+ let!(:theme) { upload_theme_component }
+
+ fab!(:category)
+ fab!(:tag)
+
+ fab!(:topic_1) { Fabricate(:topic) }
+ fab!(:topic_2) { Fabricate(:topic, category: category, tags: [tag]) }
+
+ fab!(:post_1) {
+ Fabricate(:post, raw: "\n\n# Heading 1\nContent for the first heading\n## Heading 2\nContent for the second heading\n### Heading 3\nContent for the third heading\n# Heading 4\nContent for the fourth heading", topic: topic_1)
+ }
+
+ fab!(:post_2) {
+ Fabricate(:post, raw: "\n# Heading 1\nContent for the first heading\n## Heading 2\nContent for the second heading\n### Heading 3\nContent for the third heading\n# Heading 4\nContent for the fourth heading", topic: topic_2)
+ }
+
+ fab!(:post_3) {
+ Fabricate(:post, raw: "intentionally \n long \n content \n so \n there's \n plenty \n to be \n scrolled \n past \n which \n will \n force \n the \n timeline \n to \n hide \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll ", topic: topic_1)
+ }
+
+ it "table of contents button appears in mobile view" do
+ visit("/t/#{topic_1.id}/?mobile_view=1")
+
+ expect(page).to have_css(".d-toc-mini")
+ end
+
+ it "clicking the toggle button toggles the timeline" do
+ visit("/t/#{topic_1.id}/?mobile_view=1")
+
+ find(".d-toc-mini").click
+
+ expect(page).to have_css(".d-toc-wrapper.overlay")
+ end
+
+ it "timeline toggle does not appear when the progress bar timeline is expanded" do
+ visit("/t/#{topic_1.id}/?mobile_view=1")
+
+ find("#topic-progress").click
+
+ expect(page).to have_no_css(".timeline-toggle")
+ end
+
+ it "d-toc-mini is hidden when scrolled past the first post" do
+ visit("/t/#{topic_1.id}/?mobile_view=1")
+
+ page.execute_script <<~JS
+ window.scrollTo(0, document.body.scrollHeight);
+ JS
+
+ expect(page).to have_css(".d-toc-mini")
+ end
+
+ it "d-toc-mini does not appear if the first post does not contain the markup" do
+ visit("/t/#{topic_2.id}/?mobile_view=1")
+
+ expect(page).to have_no_css(".d-toc-mini")
+ end
+
+ it "d-toc-mini will appear without markup if auto_TOC_categories is set to the topic's category" do
+ theme.update_setting(:auto_TOC_categories, "#{category.id}" )
+ theme.save!
+
+ visit("/t/#{topic_2.id}/?mobile_view=1")
+
+ expect(page).to have_css(".d-toc-mini")
+ end
+
+ it "d-toc-mini will not appear automatically if auto_TOC_categories is set to a different category" do
+ theme.update_setting(:auto_TOC_categories, "99" )
+ theme.save!
+
+ visit("/t/#{topic_2.id}/?mobile_view=1")
+
+ expect(page).to have_no_css(".d-toc-mini")
+ end
+
+ it "d-toc-mini will appear without markup if auto_TOC_tags is set to the topic's tag" do
+ theme.update_setting(:auto_TOC_tags, "#{tag.name}" )
+ theme.save!
+
+ visit("/t/#{topic_2.id}/?mobile_view=1")
+
+ expect(page).to have_css(".d-toc-mini")
+ end
+
+ it "d-toc-mini will not appear automatically if auto_TOC_tags is set to a different tag" do
+ theme.update_setting(:auto_TOC_tags, "wrong-tag" )
+ theme.save!
+
+ visit("/t/#{topic_2.id}/?mobile_view=1")
+
+ expect(page).to have_no_css(".d-toc-mini")
+ end
+
+ it "d-toc-mini does not appear if it has fewer headings than TOC_min_heading setting" do
+ theme.update_setting(:TOC_min_heading, 5)
+ theme.save!
+
+ visit("/t/#{topic_1.id}/?mobile_view=1")
+
+ expect(page).to have_no_css(".d-toc-mini")
+ end
+end
diff --git a/spec/system/discotoc_timeline_user_spec.rb b/spec/system/discotoc_timeline_user_spec.rb
new file mode 100644
index 0000000..4d5fb3d
--- /dev/null
+++ b/spec/system/discotoc_timeline_user_spec.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+RSpec.describe "DiscoTOC", system: true do
+ let!(:theme) { upload_theme_component }
+
+ fab!(:category)
+ fab!(:tag)
+
+ fab!(:topic_1) { Fabricate(:topic) }
+ fab!(:topic_2) { Fabricate(:topic, category: category, tags: [tag]) }
+
+ fab!(:post_1) {
+ Fabricate(:post, raw: "\n\n# Heading 1\nContent for the first heading\n## Heading 2\nContent for the second heading\n### Heading 3\nContent for the third heading\n# Heading 4\nContent for the fourth heading", topic: topic_1)
+ }
+
+ fab!(:post_2) {
+ Fabricate(:post, raw: "\n# Heading 1\nContent for the first heading\n## Heading 2\nContent for the second heading\n### Heading 3\nContent for the third heading\n# Heading 4\nContent for the fourth heading", topic: topic_2)
+ }
+
+ fab!(:post_3) {
+ Fabricate(:post, raw: "intentionally \n long \n content \n so \n there's \n plenty \n to be \n scrolled \n past \n which \n will \n force \n the \n timeline \n to \n hide \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll ", topic: topic_1)
+ }
+
+ it "table of contents appears when the relevant markup is added to first post in topic" do
+ visit("/t/#{topic_1.id}")
+
+ expect(page).to have_css(".d-toc-item.d-toc-h1")
+ end
+
+ it "clicking the toggle button toggles the timeline" do
+ visit("/t/#{topic_1.id}")
+
+ find(".timeline-toggle").click
+
+ expect(page).to have_css(".timeline-scrollarea-wrapper")
+
+ find(".timeline-toggle").click
+
+ expect(page).to have_css(".d-toc-item.d-toc-h1")
+ end
+
+ it "timeline does not appear when the table of contents is shown" do
+ visit("/t/#{topic_1.id}")
+
+ expect(page).to have_no_css(".topic-timeline")
+ end
+
+ it "table of contents is hidden when scrolled past the first post" do
+ visit("/t/#{topic_1.id}")
+
+ page.execute_script <<~JS
+ window.scrollTo(0, document.body.scrollHeight);
+ JS
+
+ expect(page).to have_css(".topic-timeline")
+ end
+
+ it "table of contents does not appear if the first post does not contain the markup" do
+ visit("/t/#{topic_2.id}")
+
+ expect(page).to have_no_css(".d-toc-item.d-toc-h1")
+ end
+
+ it "timeline will appear without markup if auto_TOC_categories is set to the topic's category" do
+ theme.update_setting(:auto_TOC_categories, "#{category.id}" )
+ theme.save!
+
+ visit("/t/#{topic_2.id}")
+
+ expect(page).to have_css(".d-toc-item.d-toc-h1")
+ end
+
+ it "timeline will not appear automatically if auto_TOC_categories is set to a different category" do
+ theme.update_setting(:auto_TOC_categories, "99" )
+ theme.save!
+
+ visit("/t/#{topic_2.id}")
+
+ expect(page).to have_no_css(".d-toc-item.d-toc-h1")
+ end
+
+ it "timeline will appear without markup if auto_TOC_tags is set to the topic's tag" do
+ theme.update_setting(:auto_TOC_tags, "#{tag.name}" )
+ theme.save!
+
+ visit("/t/#{topic_2.id}")
+
+ expect(page).to have_css(".d-toc-item.d-toc-h1")
+ end
+
+ it "timeline will not appear automatically if auto_TOC_tags is set to a different tag" do
+ theme.update_setting(:auto_TOC_tags, "wrong-tag" )
+ theme.save!
+
+ visit("/t/#{topic_2.id}")
+
+ expect(page).to have_no_css(".d-toc-item.d-toc-h1")
+ end
+
+ it "timeline does not appear if it has fewer headings than TOC_min_heading setting" do
+ theme.update_setting(:TOC_min_heading, 5)
+ theme.save!
+
+ visit("/t/#{topic_1.id}")
+
+ expect(page).to have_no_css(".d-toc-item.d-toc-h1")
+ end
+end
diff --git a/test/acceptance/toc-composer-test.js b/test/acceptance/toc-composer-test.js
deleted file mode 100644
index 03a4b1a..0000000
--- a/test/acceptance/toc-composer-test.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import { click, visit } from "@ember/test-helpers";
-import { test } from "qunit";
-import {
- acceptance,
- exists,
- query,
-} from "discourse/tests/helpers/qunit-helpers";
-import selectKit from "discourse/tests/helpers/select-kit-helper";
-import I18n from "discourse-i18n";
-
-acceptance("DiscoTOC - Composer", function (needs) {
- needs.user();
- needs.settings({
- general_category_id: 1,
- default_composer_category: 1,
- });
-
- test("Can use TOC when creating a topic", async function (assert) {
- await visit("/");
- await click("#create-topic");
- const toolbarPopupMenu = selectKit(".toolbar-popup-menu-options");
- await toolbarPopupMenu.expand();
- await toolbarPopupMenu.selectRowByName(
- I18n.t(themePrefix("insert_table_of_contents"))
- );
-
- assert.ok(query(".d-editor-input").value.includes('data-theme-toc="true"'));
- });
-
- test("Can use TOC when editing first post", async function (assert) {
- await visit("/t/internationalization-localization/280");
- await click("#post_1 .show-more-actions");
- await click("#post_1 .edit");
-
- assert.ok(exists("#reply-control"));
-
- const toolbarPopupMenu = selectKit(".toolbar-popup-menu-options");
- await toolbarPopupMenu.expand();
- await toolbarPopupMenu.selectRowByName(
- I18n.t(themePrefix("insert_table_of_contents"))
- );
-
- assert.ok(query(".d-editor-input").value.includes('data-theme-toc="true"'));
- });
-
- test("no TOC option when replying", async function (assert) {
- await visit("/t/internationalization-localization/280");
- await click(".create.reply");
- const toolbarPopupMenu = selectKit(".toolbar-popup-menu-options");
- await toolbarPopupMenu.expand();
-
- assert.notOk(toolbarPopupMenu.rowByValue("insertDtoc").exists());
- });
-});
diff --git a/test/acceptance/toc-test.js b/test/acceptance/toc-test.js
deleted file mode 100644
index bf34a6b..0000000
--- a/test/acceptance/toc-test.js
+++ /dev/null
@@ -1,177 +0,0 @@
-import { visit } from "@ember/test-helpers";
-import { test } from "qunit";
-import topicFixtures from "discourse/tests/fixtures/topic";
-import {
- acceptance,
- exists,
- query,
-} from "discourse/tests/helpers/qunit-helpers";
-import { cloneJSON } from "discourse-common/lib/object";
-
-const COOKED_WITH_HEADINGS =
- '
\n帖子控制
\n
\nMeasure h2
\n
Jaracaca Swamp we gazed round the very evening light in some. HTML version of science far too late. Wait a snake and nearly half-past two terrible carnivorous dinosaur and distribute. Employers Liability Act you! Each of me see that the crudest pleasantry. Sonny my own special brain. Advancing in front of them and there?
\n
\n
\n\n
\n
questions
\n
vanish
\n
contention
\n
\n\n\n
\n
nearer
\n
depressed
\n
francisca
\n
\n
\n
rooms
\n
kennel
\n
genesis
\n
\n\n
\n
\nUndeveloped h2
\n
Cried leaning upon the tangle of the full of the same. Behind us upon the luxurious. Tarp Henry of the moment that similar upon his lecture. Devil got there came well with him fifteen dollars a whisper We slunk through. You’ll find its palm. Other ones and east of Shakespeare could his seat there by. McArdle looked round and I have thrown open and his people have seen the tangled.Cried leaning upon the tangle of the full of the same. Behind us upon the luxurious. Tarp Henry of the moment that similar upon his lecture. Devil got there came well with him fifteen dollars a whisper We slunk through. You’ll find its palm. Other ones and east of Shakespeare could his seat there by. McArdle looked round and I have thrown open and his people have seen the tangled. \nCried leaning upon the tangle of the full of the same. Behind us upon the luxurious. Tarp Henry of the moment that similar upon his lecture. Devil got there came well with him fifteen dollars a whisper We slunk through. You’ll find its palm. Other ones and east of Shakespeare could his seat there by. McArdle looked round and I have thrown open and his people have seen the tangled. \nCried leaning upon the tangle of the full of the same. Behind us upon the luxurious. Tarp Henry of the moment that similar upon his lecture. Devil got there came well with him fifteen dollars a whisper We slunk through. You’ll find its palm. Other ones and east of Shakespeare could his seat there by. McArdle looked round and I have thrown open and his people have seen the tangled.
\n
\nH1 second section
\n
\nUndeveloped 2 h2
\n
Cried leaning upon the tangle of the full of the same. Behind us upon the luxurious. Tarp Henry of the moment that similar upon his lecture. Devil got there came well with him fifteen dollars a whisper We slunk through. \nYou’ll find its palm. Other ones and east of Shakespeare could his seat there by. McArdle looked round and I have thrown open and his people have seen the tangled.
\n
Cried leaning upon the tangle of the full of the same. Behind us upon the luxurious. Tarp Henry of the moment that similar upon his lecture. Devil got there came well with him fifteen dollars a whisper We slunk through. You’ll find its palm. Other ones and east of Shakespeare could his seat there by. McArdle looked round and I have thrown open and his people have seen the tangled.
\n
\nSubheading 3 h3
\n
Cried leaning upon the tangle of the full of the same. Behind us upon the luxurious. Tarp Henry of the moment that similar upon his lecture. Devil got there came well with him fifteen dollars a whisper We slunk through. You’ll find its palm. Other ones and east of Shakespeare could his seat there by. McArdle looked round and I have thrown open and his people have seen the tangled.
\n
\nSubheading 3 long ass wire h3
\n
Cried leaning upon the tangle of the full of the same. Behind us upon the luxurious. Tarp Henry of the moment that similar upon his lecture. Devil got there came well with him fifteen dollars a whisper We slunk through. You’ll find its palm. Other ones and east of Shakespeare could his seat there by. McArdle looked round and I have thrown open and his people have seen the tangled.
\n
\nAnother section h2
\n
Cried leaning upon the tangle of the full of the same. Behind us upon the luxurious. Tarp Henry of the moment that similar upon his lecture. Devil got there came well with him fifteen dollars a whisper We slunk through. You’ll find its palm. Other ones and east of Shakespeare could his seat there by. McArdle looked round and I have thrown open and his people have seen the tangled.
\n
\nSubheading again then h3
\n
Cried leaning upon the tangle of the full of the same. Behind us upon the luxurious. Tarp Henry of the moment that similar upon his lecture. Devil got there came well with him fifteen dollars a whisper We slunk through. You’ll find its palm. Other ones and east of Shakespeare could his seat there by. McArdle looked round and I have thrown open and his people have seen the tangled.
\n
\nSu-subbheading h4
\n
Cried leaning upon the tangle of the full of the same. Behind us upon the luxurious. Tarp Henry of the moment that similar upon his lecture. Devil got there came well with him fifteen dollars a whisper We slunk through. You’ll find its palm. Other ones and east of Shakespeare could his seat there by. McArdle looked round and I have thrown open and his people have seen the tangled.
\n
Cried leaning upon the tangle of the full of the same. Behind us upon the luxurious. Tarp Henry of the moment that similar upon his lecture. Devil got there came well with him fifteen dollars a whisper We slunk through. You’ll find its palm. Other ones and east of Shakespeare could his seat there by. McArdle looked round and I have thrown open and his people have seen the tangled.
\n
\nSu-subalicions heading h4
\n
Cried leaning upon the tangle of the full of the same. Behind us upon the luxurious. Tarp Henry of the moment that similar upon his lecture. Devil got there came well with him fifteen dollars a whisper We slunk through. You’ll find its palm. Other ones and east of Shakespeare could his seat there by. McArdle looked round and I have thrown open and his people have seen the tangled.
\n
Cried leaning upon the tangle of the full of the same. Behind us upon the luxurious. Tarp Henry of the moment that similar upon his lecture. Devil got there came well with him fifteen dollars a whisper We slunk through. You’ll find its palm. Other ones and east of Shakespeare could his seat there by. McArdle looked round and I have thrown open and his people have seen the tangled.
\n
\nSu-subalicions heading h4 quite long to test a real-life kind of scenario here then
\n
Cried leaning upon the tangle of the full of the same. Behind us upon the luxurious. Tarp Henry of the moment that similar upon his lecture. Devil got there came well with him fifteen dollars a whisper We slunk through. You’ll find its palm. Other ones and east of Shakespeare could his seat there by. McArdle looked round and I have thrown open and his people have seen the tangled.
\n
Cried leaning upon the tangle of the full of the same. Behind us upon the luxurious. Tarp Henry of the moment that similar upon his lecture. Devil got there came well with him fifteen dollars a whisper We slunk through. You’ll find its palm. Other ones and east of Shakespeare could his seat there by. McArdle looked round and I have thrown open and his people have seen the tangled.
\n
\nSu-subalicions heading h4 also quite long to test a real-life kind of scenario here then
\n
Cried leaning upon the tangle of the full of the same. Behind us upon the luxurious. Tarp Henry of the moment that similar upon his lecture. Devil got there came well with him fifteen dollars a whisper We slunk through. You’ll find its palm. Other ones and east of Shakespeare could his seat there by. McArdle looked round and I have thrown open and his people have seen the tangled.
\n
Cried leaning upon the tangle of the full of the same. Behind us upon the luxurious. Tarp Henry of the moment that similar upon his lecture. Devil got there came well with him fifteen dollars a whisper We slunk through. You’ll find its palm. Other ones and east of Shakespeare could his seat there by. McArdle looked round and I have thrown open and his people have seen the tangled.