From 7c19d4186414a35dd65e8f5ad6950b3561cb6d21 Mon Sep 17 00:00:00 2001 From: Kris Date: Wed, 25 Oct 2023 12:08:24 -0400 Subject: [PATCH] FEATURE: Toggle between timeline and TOC (#64) --- assets/sprite.svg | 15 ++- common/common.scss | 120 ++++++++++++++--- .../after-topic-progress/d-toc-mini.hbs | 7 - .../before-topic-progress/d-toc-mini.hbs | 3 + .../d-toc-mini.js | 5 + .../discourse/initializers/disco-toc-main.js | 125 +++++++++++++++--- locales/en.yml | 4 +- test/acceptance/toc-test.js | 10 +- 8 files changed, 240 insertions(+), 49 deletions(-) delete mode 100644 javascripts/discourse/connectors/after-topic-progress/d-toc-mini.hbs create mode 100644 javascripts/discourse/connectors/before-topic-progress/d-toc-mini.hbs rename javascripts/discourse/connectors/{after-topic-progress => before-topic-progress}/d-toc-mini.js (72%) diff --git a/assets/sprite.svg b/assets/sprite.svg index ea133f1..2542006 100644 --- a/assets/sprite.svg +++ b/assets/sprite.svg @@ -1,5 +1,5 @@ - - + + @@ -14,5 +14,12 @@ - - + + + + + + + + + diff --git a/common/common.scss b/common/common.scss index 55e8502..b56e730 100644 --- a/common/common.scss +++ b/common/common.scss @@ -1,26 +1,35 @@ $padding-basis: 0.75em; -.d-toc-main { - display: none; +.d-toc-available .d-toc-wrapper { + // the toc increases the timeline container's width + // so as long as the toc is available in a topic + // we want this width to remain for the timeline + // so if the toc is toggled off, the position doesn't shift width: 225px; @media screen and (max-width: 1045px) { .desktop-view & { width: 150px; } } +} + +.d-toc-main { + display: none; border-left: 1px solid var(--primary-low); box-sizing: border-box; a { display: block; padding: 0.15em 0; - color: var(--primary-medium); + color: var(--primary-700); &.scroll-to-bottom { + color: var(--tertiary); padding-left: $padding-basis; + margin-top: 0.5em; + font-size: var(--font-down-1); } } #d-toc { - max-height: calc(100vh - 4.5em - var(--header-offset)); - padding-bottom: 0.5em; + max-height: calc(100dvh - 7em - var(--header-offset)); overflow: auto; ul { list-style-type: none; @@ -47,7 +56,7 @@ $padding-basis: 0.75em; } } > a:hover { - color: var(--primary-high); + color: var(--primary); } &.direct-active > a { position: relative; @@ -124,6 +133,53 @@ a.d-toc-close { display: none; } +.d-toc-timeline-toggle { + display: none; + position: fixed; + bottom: 0; + @include ellipsis; + .d-icon-timeline { + margin-right: 0.25em; + } + + @media screen and (min-height: 1200px) { + // on tall screens we don't want this button + // to be incredibly distant + position: absolute; + .d-toc-timeline-visible & { + bottom: -4em; + } + bottom: -6.5em; + } +} + +.topic-navigation.with-timeline:has(.timeline-docked) { + // hide toggle when timeline is docked + // (firefox doesn't support :has yet) + .d-toc-timeline-toggle { + display: none; + } +} + +.d-toc-timeline-toggleable { + .d-toc-timeline-toggle { + display: block; + z-index: z("timeline"); + @media screen and (max-height: 480px) { + // avoid overlapping timeline + display: none; + } + } + .with-topic-progress { + .d-toc-main { + display: block; + } + .d-toc-timeline-toggle { + display: none; + } + } +} + .d-toc-timeline-visible { .d-toc-main, .d-toc-mini { @@ -134,7 +190,7 @@ a.d-toc-close { .d-toc-wrapper { position: fixed; margin-top: 0.25em; - height: calc(100vh - 50px - var(--header-offset)); + height: calc(100dvh - 50px - var(--header-offset)); opacity: 0.5; right: -100vw; top: var(--header-offset); @@ -149,18 +205,25 @@ a.d-toc-close { padding: 0.5em; height: 100%; #d-toc { - max-height: calc(100% - 3em); + max-height: calc(100% - 2.25em); } } &.overlay { right: 0; width: 75vw; opacity: 1; - .d-toc-main #d-toc li.d-toc-item ul { - transition: none; + .d-toc-main { + display: block; + #d-toc li.d-toc-item ul { + transition: none; + } } } + a.scroll-to-bottom { + margin-top: 0.33em; + } + a.scroll-to-bottom, a.d-toc-close { display: inline-block; @@ -168,14 +231,17 @@ a.d-toc-close { } .d-toc-icons { - text-align: right; + position: absolute; + background: var(--secondary); + right: 1.5em; + top: 0.25em; + z-index: z("timeline"); } } } // core overrides when timeline is active - .timeline-container, - #topic-progress { + .timeline-container { display: none; } .container.posts .topic-navigation.with-topic-progress { @@ -183,6 +249,26 @@ a.d-toc-close { } } +#topic-progress-wrapper { + align-items: stretch; + .d-toc-mini { + display: none; + .d-toc-timeline-visible & { + display: block; + } + height: 100%; + .btn { + height: 100%; + } + } + + .staff & { + .topic-admin-menu-button-container { + margin-left: 0.5em; + } + } +} + // core sets first child's top margin to 0 // ensure it's also 0 when TOC markup is first .cooked > div[data-theme-toc]:first-child + * { @@ -232,11 +318,5 @@ a.d-toc-close { .below-docs-topic-outlet.d-toc-wrapper { position: sticky; top: calc(var(--header-offset, 60px) + 1em); - max-height: calc(100vh - 2em - var(--header-offset, 60px)); - .mobile-view & { - display: none; - } - .d-toc-main { - display: block; - } + max-height: calc(100dvh - 2em - var(--header-offset, 60px)); } diff --git a/javascripts/discourse/connectors/after-topic-progress/d-toc-mini.hbs b/javascripts/discourse/connectors/after-topic-progress/d-toc-mini.hbs deleted file mode 100644 index b26df5d..0000000 --- a/javascripts/discourse/connectors/after-topic-progress/d-toc-mini.hbs +++ /dev/null @@ -1,7 +0,0 @@ -
- -
\ No newline at end of file diff --git a/javascripts/discourse/connectors/before-topic-progress/d-toc-mini.hbs b/javascripts/discourse/connectors/before-topic-progress/d-toc-mini.hbs new file mode 100644 index 0000000..93dddd4 --- /dev/null +++ b/javascripts/discourse/connectors/before-topic-progress/d-toc-mini.hbs @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/javascripts/discourse/connectors/after-topic-progress/d-toc-mini.js b/javascripts/discourse/connectors/before-topic-progress/d-toc-mini.js similarity index 72% rename from javascripts/discourse/connectors/after-topic-progress/d-toc-mini.js rename to javascripts/discourse/connectors/before-topic-progress/d-toc-mini.js index 41476f0..3f89217 100644 --- a/javascripts/discourse/connectors/after-topic-progress/d-toc-mini.js +++ b/javascripts/discourse/connectors/before-topic-progress/d-toc-mini.js @@ -6,4 +6,9 @@ export default class DTocMini extends Component { showTOCOverlay() { document.querySelector(".d-toc-wrapper").classList.toggle("overlay"); } + + @action + resetBodyClass() { + document.body.classList.add("d-toc-timeline-visible"); + } } diff --git a/javascripts/discourse/initializers/disco-toc-main.js b/javascripts/discourse/initializers/disco-toc-main.js index e9a47f3..e24c279 100644 --- a/javascripts/discourse/initializers/disco-toc-main.js +++ b/javascripts/discourse/initializers/disco-toc-main.js @@ -6,6 +6,8 @@ import { slugify } from "discourse/lib/utilities"; import { withPluginApi } from "discourse/lib/plugin-api"; import I18n from "I18n"; +let TOChidden = false; + export default { name: "disco-toc-main", @@ -80,13 +82,29 @@ export default { ); api.onAppEvent("topic:current-post-changed", (args) => { + // manages the timeline area width via CSS if (!document.querySelector(".d-toc-cooked")) { - return; - } - if (args.post.post_number === 1) { - document.body.classList.add("d-toc-timeline-visible"); + return document.body.classList.remove("d-toc-available"); } else { - document.body.classList.remove("d-toc-timeline-visible"); + document.body.classList.add("d-toc-available"); + } + + // manages timeline visibility + if (args.post.post_number === 1) { + if (!TOChidden) { + handleButtonAndBody("show"); + } + + // don't show the toggle if there's only 1 post + if (args.post.topic.posts_count !== 1) { + document.body.classList.add("d-toc-timeline-toggleable"); + } + } else { + handleButtonAndBody("hide"); + } + + if (args.post.topic.posts_count === 1) { + document.body.classList.remove("d-toc-timeline-toggleable"); } }); @@ -103,7 +121,8 @@ export default { }); api.cleanupStream(() => { - document.body.classList.remove("d-toc-timeline-visible"); + handleButtonAndBody("hide"); + TOChidden = false; document.removeEventListener("click", this.clickTOC, false); }); }); @@ -150,30 +169,67 @@ export default { } }, - insertTOC(headings) { + createMainDiv() { const dToc = document.createElement("div"); dToc.classList.add("d-toc-main"); - dToc.innerHTML = `
- ${iconHTML("downward")} - ${iconHTML("times")}
`; + dToc.innerHTML = `
${iconHTML( + "times" + )}
`; + return dToc; + }, + + createScrollLink() { + const scrollLink = document.createElement("a"); + scrollLink.href = "#"; + scrollLink.className = "scroll-to-bottom"; + scrollLink.title = I18n.t(themePrefix("post_bottom_tooltip")); + scrollLink.innerHTML = `${iconHTML("downward")} ${I18n.t( + themePrefix("jump_bottom") + )}`; + return scrollLink; + }, + + createToggleButton() { + const toggleButton = document.createElement("button"); + toggleButton.className = + "d-toc-timeline-toggle btn btn-default btn-icon-text"; + toggleButton.innerHTML = + iconHTML("timeline") + I18n.t(themePrefix("topic_timeline")); + + return toggleButton; + }, + + insertTOC(headings) { + const dToc = this.createMainDiv(); + const scrollLink = this.createScrollLink(); + this.toggleButton = this.createToggleButton(); const existing = document.querySelector(".d-toc-wrapper .d-toc-main"); + const wrapper = document.querySelector(".d-toc-wrapper"); + if (existing) { - document.querySelector(".d-toc-wrapper").replaceChild(dToc, existing); + wrapper.replaceChild(dToc, existing); } else { - document.querySelector(".d-toc-wrapper").appendChild(dToc); + wrapper.appendChild(dToc); } const result = this.buildTOC(Array.from(headings)); - document.querySelector(".d-toc-main").appendChild(result); + dToc.appendChild(result); + dToc.appendChild(scrollLink); + wrapper.appendChild(this.toggleButton); document.addEventListener("click", this.clickTOC, false); }, clickTOC(e) { const classNames = ["d-toc-timeline-visible", "archetype-docs-topic"]; + // toggle timeline visibility + if (e.target.closest(".d-toc-timeline-toggle")) { + handleButtonAndBody("toggle"); + e.preventDefault(); + return false; + } + if ( !classNames.some((className) => document.body.classList.contains(className) @@ -212,7 +268,7 @@ export default { top: rect.bottom + window.scrollY - headerOffset() - 10, behavior: "smooth", }); - + document.querySelector(".d-toc-wrapper").classList.remove("overlay"); e.preventDefault(); return false; } @@ -317,3 +373,40 @@ function parentsUntil(el, selector, filter) { } return result; } + +function handleButtonAndBody(action) { + const body = document.body; + const button = document.querySelector("button.d-toc-timeline-toggle"); + + switch (action) { + case "toggle": + body.classList.toggle("d-toc-timeline-visible"); + TOChidden = !TOChidden; + break; + + case "hide": + body.classList.remove( + "d-toc-timeline-visible", + "d-toc-timeline-toggleable" + ); + break; + + case "show": + body.classList.add("d-toc-timeline-visible", "d-toc-timeline-toggleable"); + break; + } + + if (button) { + const translationKey = body.classList.contains("d-toc-timeline-visible") + ? "topic_timeline" + : "table_of_contents"; + + const icon = body.classList.contains("d-toc-timeline-visible") + ? "timeline" + : "stream"; + + button.innerHTML = `${iconHTML(icon)}${I18n.t( + themePrefix(translationKey) + )} `; + } +} diff --git a/locales/en.yml b/locales/en.yml index 7b587ec..594af2a 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -1,5 +1,7 @@ en: - table_of_contents: table of contents + table_of_contents: Table of contents + topic_timeline: Topic timeline + jump_bottom: Jump to replies insert_table_of_contents: Insert table of contents post_bottom_tooltip: Navigate to post controls theme_metadata: diff --git a/test/acceptance/toc-test.js b/test/acceptance/toc-test.js index 17d5e62..0bc7850 100644 --- a/test/acceptance/toc-test.js +++ b/test/acceptance/toc-test.js @@ -3,7 +3,7 @@ import { exists, query, } from "discourse/tests/helpers/qunit-helpers"; -import { visit } from "@ember/test-helpers"; +import { click, visit } from "@ember/test-helpers"; import { test } from "qunit"; import topicFixtures from "discourse/tests/fixtures/topic"; import { cloneJSON } from "discourse-common/lib/object"; @@ -66,6 +66,14 @@ acceptance("DiscoTOC - main", function (needs) { "heading gets an ID even when it has no Latin characters" ); }); + + test("TOC can be toggled to reveal timeline", async function (assert) { + await visit("/t/internationalization-localization/280"); + assert.ok(exists(".d-toc-timeline-toggle"), "TOC toggle exists"); + + await click(".d-toc-timeline-toggle"); + assert.ok(exists(".topic-timeline"), "The timeline is shown on toggle"); + }); }); acceptance("DiscoTOC - off", function (needs) {