REFACTOR: General component overhaul (#19)
This commit is contained in:
parent
5b2f5a455e
commit
20366c671d
|
@ -1 +1,2 @@
|
|||
2.7.13: 5b2f5a455e1adf8ce5e8c1cfb7fbc3c388d3d82a
|
||||
2.6.0.beta3: 68d40fe9f5b625cf465adc31b502a54e16d02cc6
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "eslint-config-discourse",
|
||||
"ignorePatterns": [
|
||||
"javascripts/vendor/*"
|
||||
],
|
||||
"globals": {
|
||||
"settings": "readonly",
|
||||
"themePrefix": "readonly"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
name: Linting
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
|
||||
- name: Yarn install
|
||||
run: yarn install
|
||||
|
||||
- name: ESLint
|
||||
if: ${{ always() }}
|
||||
run: yarn eslint --ext .js,.js.es6 --no-error-on-unmatched-pattern {test,javascripts}
|
||||
|
||||
- name: Prettier
|
||||
if: ${{ always() }}
|
||||
shell: bash
|
||||
run: |
|
||||
yarn prettier -v
|
||||
shopt -s extglob
|
||||
if ls @(javascripts|desktop|mobile|common|scss)/**/*.@(scss|js|es6) &> /dev/null; then
|
||||
yarn prettier --list-different "@(javascripts|desktop|mobile|common|scss)/**/*.{scss,js,es6}"
|
||||
fi
|
||||
if ls test/**/*.@(js|es6) &> /dev/null; then
|
||||
yarn prettier --list-different "test/**/*.{js,es6}"
|
||||
fi
|
||||
|
||||
- name: Ember template lint
|
||||
if: ${{ always() }}
|
||||
run: yarn ember-template-lint javascripts
|
|
@ -0,0 +1,121 @@
|
|||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container: discourse/discourse_test:slim-browsers
|
||||
timeout-minutes: 30
|
||||
|
||||
env:
|
||||
DISCOURSE_HOSTNAME: www.example.com
|
||||
RUBY_GLOBAL_METHOD_CACHE_SIZE: 131072
|
||||
RAILS_ENV: development
|
||||
QUNIT_RAILS_ENV: development
|
||||
PGHOST: postgres
|
||||
PGUSER: discourse
|
||||
PGPASSWORD: discourse
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:13
|
||||
ports:
|
||||
- 5432:5432
|
||||
env:
|
||||
POSTGRES_USER: discourse
|
||||
POSTGRES_PASSWORD: discourse
|
||||
options: >-
|
||||
--mount type=tmpfs,destination=/var/lib/postgresql/data
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
repository: discourse/discourse
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Install component
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
path: tmp/component
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup Git
|
||||
run: |
|
||||
git config --global user.email "ci@ci.invalid"
|
||||
git config --global user.name "Discourse CI"
|
||||
|
||||
- name: Start redis
|
||||
run: |
|
||||
redis-server /etc/redis/redis.conf &
|
||||
|
||||
- name: Start Postgres
|
||||
run: |
|
||||
chown -R postgres /var/run/postgresql
|
||||
sudo -E -u postgres script/start_test_db.rb
|
||||
sudo -u postgres psql -c "CREATE ROLE $PGUSER LOGIN SUPERUSER PASSWORD '$PGPASSWORD';"
|
||||
|
||||
- name: Bundler cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: vendor/bundle
|
||||
key: ${{ runner.os }}-2.7-gem-${{ hashFiles('**/Gemfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-2.7-gem-
|
||||
|
||||
- name: Setup gems
|
||||
run: |
|
||||
bundle config --local path vendor/bundle
|
||||
bundle config --local deployment true
|
||||
bundle config --local without development
|
||||
bundle install --jobs 4
|
||||
bundle clean
|
||||
|
||||
- name: Get yarn cache directory
|
||||
id: yarn-cache-dir
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- name: Yarn cache
|
||||
uses: actions/cache@v2
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir.outputs.dir }}
|
||||
key: ${{ runner.os }}-${{ matrix.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ matrix.os }}-yarn-
|
||||
|
||||
- name: Yarn install
|
||||
run: yarn install
|
||||
|
||||
- name: Migrate database
|
||||
run: |
|
||||
bin/rake db:create
|
||||
bin/rake db:migrate
|
||||
|
||||
- name: Check qunit existence
|
||||
id: check_qunit
|
||||
shell: bash
|
||||
run: |
|
||||
shopt -s extglob
|
||||
if ls tmp/component/test/**/*.@(js|es6) &> /dev/null; then
|
||||
echo "::set-output name=files_exist::true"
|
||||
fi
|
||||
|
||||
- name: Component QUnit
|
||||
if: steps.check_qunit.outputs.files_exist == 'true'
|
||||
run: |
|
||||
bundle exec rake themes:install -- '--{"${{ github.event.repository.name }}": "tmp/component"}'
|
||||
UNICORN_TIMEOUT=120 bundle exec rake themes:qunit[name,${{ github.event.repository.name }}]
|
||||
timeout-minutes: 10
|
||||
|
||||
- name: Lint English locale
|
||||
if: ${{ always() }}
|
||||
run: bundle exec ruby script/i18n_lint.rb "tmp/component/locales/en.yml"
|
|
@ -0,0 +1,3 @@
|
|||
.discourse-site
|
||||
.DS_Store
|
||||
node_modules
|
|
@ -0,0 +1,4 @@
|
|||
module.exports = {
|
||||
plugins: ["ember-template-lint-plugin-discourse"],
|
||||
extends: "discourse:recommended",
|
||||
};
|
|
@ -1,339 +1,207 @@
|
|||
@import "common/foundation/variables";
|
||||
@import "sidebar";
|
||||
@import "chat";
|
||||
$padding-basis: 0.75em;
|
||||
|
||||
.d-toc-regular {
|
||||
[data-theme-toc] {
|
||||
display: none;
|
||||
.d-toc-main {
|
||||
display: none;
|
||||
width: 225px;
|
||||
border-left: 1px solid var(--primary-low);
|
||||
padding-bottom: 0.5em;
|
||||
box-sizing: border-box;
|
||||
a {
|
||||
display: block;
|
||||
padding: 0.15em 0;
|
||||
color: var(--primary-medium);
|
||||
&.scroll-to-bottom {
|
||||
padding-left: $padding-basis;
|
||||
}
|
||||
}
|
||||
.d-toc-ignore {
|
||||
font-size: $font-up-1;
|
||||
margin: 0 0 10px 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
.highlighted {
|
||||
animation: fadein 1s;
|
||||
}
|
||||
.d-toc {
|
||||
transform: translate3d(0, 0, 0);
|
||||
transition: opacity 0.25s;
|
||||
ul,
|
||||
li {
|
||||
list-style: none;
|
||||
#d-toc {
|
||||
max-height: calc(100vh - 3em - var(--header-offset));
|
||||
overflow: auto;
|
||||
ul {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
.d-toc-item {
|
||||
padding: 6px 0;
|
||||
a {
|
||||
color: var(--primary-high);
|
||||
li.d-toc-item {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding-left: $padding-basis;
|
||||
line-height: 1.6em;
|
||||
> ul {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.3s ease-in-out, max-height 0.3s ease-in-out;
|
||||
}
|
||||
&.d-toc-active {
|
||||
&.active,
|
||||
.d-toc-wrapper.overlay & {
|
||||
ul {
|
||||
max-height: 50em;
|
||||
overflow: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&.direct-active > a {
|
||||
position: relative;
|
||||
color: var(--primary);
|
||||
&:before {
|
||||
:not(.rtl) & {
|
||||
border-left: 1px solid var(--tertiary);
|
||||
}
|
||||
.rtl & {
|
||||
border-right: 1px solid var(--tertiary);
|
||||
}
|
||||
height: 100%;
|
||||
content: "";
|
||||
width: 1px;
|
||||
margin-top: -1px;
|
||||
background-color: var(--tertiary);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
a {
|
||||
color: var(--primary);
|
||||
text-shadow: 0.1px 0.1px var(--primary),
|
||||
-0.1px -0.1px var(--primary);
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.d-toc-heading {
|
||||
:not(.rtl) & {
|
||||
padding-left: 10px;
|
||||
}
|
||||
.rtl & {
|
||||
padding-right: 10px;
|
||||
}
|
||||
.d-toc-active:before {
|
||||
:not(.rtl) & {
|
||||
left: -10px;
|
||||
}
|
||||
.rtl & {
|
||||
right: -10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.d-toc-subheading {
|
||||
:not(.rtl) & {
|
||||
padding-left: 20px;
|
||||
}
|
||||
.rtl & {
|
||||
padding-right: 20px;
|
||||
}
|
||||
.d-toc-active:before {
|
||||
:not(.rtl) & {
|
||||
left: -30px;
|
||||
}
|
||||
.rtl & {
|
||||
right: -30px;
|
||||
}
|
||||
}
|
||||
.d-toc-subheading {
|
||||
:not(.rtl) & {
|
||||
padding-left: 30px;
|
||||
}
|
||||
.rtl & {
|
||||
padding-right: 30px;
|
||||
}
|
||||
.d-toc-active:before {
|
||||
:not(.rtl) & {
|
||||
left: -60px;
|
||||
}
|
||||
.rtl & {
|
||||
right: -60px;
|
||||
}
|
||||
}
|
||||
.d-toc-subheading {
|
||||
:not(.rtl) & {
|
||||
padding-left: 40px;
|
||||
}
|
||||
.rtl & {
|
||||
padding-right: 40px;
|
||||
}
|
||||
.d-toc-active:before {
|
||||
:not(.rtl) & {
|
||||
left: -70px;
|
||||
}
|
||||
.rtl & {
|
||||
right: -70px;
|
||||
}
|
||||
}
|
||||
.d-toc-subheading {
|
||||
:not(.rtl) & {
|
||||
padding-left: 50px;
|
||||
}
|
||||
.rtl & {
|
||||
padding-right: 50px;
|
||||
}
|
||||
.d-toc-active:before {
|
||||
:not(.rtl) & {
|
||||
left: -80px;
|
||||
}
|
||||
.rtl & {
|
||||
right: -80px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.d-toc-subheading li {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
#bottom-anchor {
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.post-bottom-wrapper {
|
||||
a {
|
||||
color: var(--primary-med-or-secondary-med);
|
||||
> ul > li > ul {
|
||||
font-size: var(--font-down-1);
|
||||
li.direct-active > a:before {
|
||||
// it's odd that we need this
|
||||
margin-left: -1px;
|
||||
}
|
||||
ul {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// large screens
|
||||
@media screen and (min-width: $large-width) {
|
||||
.d-toc {
|
||||
margin-top: 1em;
|
||||
// active line marker
|
||||
$selector: "> ul > li.direct-active > a:before";
|
||||
$sub: "> ul > li";
|
||||
$map: (
|
||||
"left": "html:not(.rtl)",
|
||||
"right": "html.rtl",
|
||||
);
|
||||
|
||||
/*
|
||||
// loop below generates styling for non-RTL and RTL
|
||||
// Example:
|
||||
html:not(.rtl) SELECTOR {
|
||||
left: -.75em
|
||||
}
|
||||
html.rtl SELECTOR {
|
||||
right: -.75em
|
||||
}
|
||||
*/
|
||||
@each $prop, $parent in $map {
|
||||
#{$parent} #d-toc {
|
||||
#{$selector} {
|
||||
#{$prop}: (-$padding-basis);
|
||||
}
|
||||
#{$sub} #{$selector} {
|
||||
#{$prop}: (-$padding-basis) * 2;
|
||||
}
|
||||
#{$sub} #{$sub} #{$selector} {
|
||||
#{$prop}: (-$padding-basis) * 3;
|
||||
}
|
||||
#{$sub} #{$sub} #{$sub} #{$selector} {
|
||||
#{$prop}: (-$padding-basis) * 4;
|
||||
}
|
||||
#{$sub} #{$sub} #{$sub} #{$sub} #{$selector} {
|
||||
#{$prop}: (-$padding-basis) * 5;
|
||||
}
|
||||
#{$sub} #{$sub} #{$sub} #{$sub} #{$sub} #{$selector} {
|
||||
#{$prop}: (-$padding-basis) * 6;
|
||||
}
|
||||
}
|
||||
.post-bottom-wrapper {
|
||||
padding: 1em 0.5em 0 0.5em;
|
||||
&.mobile {
|
||||
display: none;
|
||||
}
|
||||
// END active line marker
|
||||
|
||||
.d-toc-mini,
|
||||
a.d-toc-close {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.d-toc-timeline-visible {
|
||||
.d-toc-main,
|
||||
.d-toc-mini {
|
||||
display: block;
|
||||
}
|
||||
// overlayed timeline (on mobile and narrow screens)
|
||||
.topic-navigation.with-topic-progress {
|
||||
.d-toc-wrapper {
|
||||
position: fixed;
|
||||
margin-top: 0.25em;
|
||||
height: calc(100vh - 50px - var(--header-offset));
|
||||
opacity: 0.5;
|
||||
right: -100vw;
|
||||
top: var(--header-offset);
|
||||
width: 75vw;
|
||||
max-width: 350px;
|
||||
background-color: var(--secondary);
|
||||
box-shadow: shadow("dropdown");
|
||||
z-index: z("modal", "overlay");
|
||||
transition: all 0.2s ease-in-out;
|
||||
.d-toc-main {
|
||||
width: 100%;
|
||||
padding: 0.5em;
|
||||
height: 100%;
|
||||
#d-toc {
|
||||
max-height: calc(100% - 3em);
|
||||
}
|
||||
}
|
||||
&.overlay {
|
||||
right: 0;
|
||||
width: 75vw;
|
||||
opacity: 1;
|
||||
.d-toc-main #d-toc li.d-toc-item ul {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
a.scroll-to-bottom,
|
||||
a.d-toc-close {
|
||||
display: inline-block;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.d-toc-icons {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.d-toc-toggle {
|
||||
display: none !important;
|
||||
}
|
||||
.d-toc-close-wrapper,
|
||||
.d-toc-subheading {
|
||||
// core overrides when timeline is active
|
||||
.timeline-container,
|
||||
#topic-progress {
|
||||
display: none;
|
||||
}
|
||||
.d-toc-post {
|
||||
.topic-body,
|
||||
.topic-avatar {
|
||||
border-top: none;
|
||||
}
|
||||
.d-toc {
|
||||
max-height: 85vh;
|
||||
.container.posts .topic-navigation.with-topic-progress {
|
||||
align-self: start;
|
||||
}
|
||||
}
|
||||
|
||||
// RTL Support
|
||||
.rtl {
|
||||
.d-toc-main {
|
||||
border-left: none;
|
||||
border-right: 1px solid var(--primary-low);
|
||||
|
||||
#d-toc li.d-toc-item,
|
||||
a.scroll-to-bottom {
|
||||
padding-left: 0;
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 75px;
|
||||
margin-bottom: 135px;
|
||||
max-width: 235px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
align-self: flex-start;
|
||||
flex: 1 1 auto;
|
||||
:not(.rtl) & {
|
||||
margin-left: -1px;
|
||||
}
|
||||
.rtl & {
|
||||
margin-right: -1px;
|
||||
}
|
||||
padding-right: $padding-basis;
|
||||
}
|
||||
.d-toc-article {
|
||||
display: flex;
|
||||
.post-notice {
|
||||
display: none;
|
||||
}
|
||||
.topic-map {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
> .row {
|
||||
:not(.rtl) & {
|
||||
border-right: 1px solid var(--primary-low);
|
||||
}
|
||||
.rtl & {
|
||||
border-left: 1px solid var(--primary-low);
|
||||
}
|
||||
}
|
||||
}
|
||||
#topic-title {
|
||||
margin-bottom: 0;
|
||||
.title-wrapper {
|
||||
border-bottom: 1px solid var(--primary-low);
|
||||
padding-bottom: 0.5em;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// small screens
|
||||
@media screen and (max-width: $large-width) {
|
||||
.d-toc-regular {
|
||||
.post-bottom-wrapper {
|
||||
padding: 1em 0.75em;
|
||||
&.desktop {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
#d-toc {
|
||||
z-index: z("header") + 1;
|
||||
background: var(--secondary);
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
max-width: 500px;
|
||||
overflow: scroll;
|
||||
transition: transform 0.5s, opacity 0.25s;
|
||||
transform: translatex(100%);
|
||||
opacity: 0;
|
||||
:not(.rtl) & {
|
||||
margin-left: -1px;
|
||||
}
|
||||
.rtl & {
|
||||
margin-right: -1px;
|
||||
}
|
||||
&.d-toc-mobile {
|
||||
transform: translatex(0);
|
||||
opacity: 1;
|
||||
}
|
||||
.d-toc-active {
|
||||
&:before {
|
||||
:not(.rtl) & {
|
||||
margin-left: -1px;
|
||||
}
|
||||
.rtl & {
|
||||
margin-right: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.d-toc-close-wrapper {
|
||||
height: 3em;
|
||||
background: var(--secondary);
|
||||
color: var(--primary-med-or-secondary-med);
|
||||
margin-bottom: 1em;
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
.d-toc-close {
|
||||
padding: 1em 0.75em;
|
||||
}
|
||||
}
|
||||
.d-toc-toggle {
|
||||
position: fixed;
|
||||
bottom: 5px;
|
||||
|
||||
padding: 0.5em 1em;
|
||||
background: var(--tertiary);
|
||||
color: var(--secondary);
|
||||
z-index: 3;
|
||||
margin-bottom: env(safe-area-inset-bottom);
|
||||
:not(.rtl) & {
|
||||
right: 16px;
|
||||
}
|
||||
.rtl & {
|
||||
left: 16px;
|
||||
}
|
||||
body.footer-nav-visible & {
|
||||
bottom: 49px;
|
||||
}
|
||||
}
|
||||
#d-toc > ul {
|
||||
:not(.rtl) & {
|
||||
margin-left: 20px;
|
||||
border-left: 1px solid var(--primary-low);
|
||||
}
|
||||
.rtl & {
|
||||
margin-right: 20px;
|
||||
border-right: 1px solid var(--primary-low);
|
||||
}
|
||||
&:last-child {
|
||||
margin-bottom: 5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.d-toc-timeline {
|
||||
.timeline-container,
|
||||
#topic-progress-wrapper {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
&.d-toc-timeline-visible {
|
||||
.timeline-container,
|
||||
#topic-progress-wrapper {
|
||||
opacity: 1;
|
||||
pointer-events: initial;
|
||||
}
|
||||
|
||||
.d-toc-timeline-visible .topic-navigation.with-topic-progress .d-toc-wrapper {
|
||||
right: unset;
|
||||
left: -100vw;
|
||||
&.overlay {
|
||||
right: unset;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Composer preview notice
|
||||
.edit-title .d-editor-preview [data-theme-toc] {
|
||||
background: var(--tertiary);
|
||||
color: var(--secondary);
|
||||
border-top: 2px solid var(--secondary);
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 30px;
|
||||
|
|
|
@ -1,395 +0,0 @@
|
|||
<script type="text/discourse-plugin" version="0.1">
|
||||
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 linkIcon = iconHTML(settings.anchor_icon);
|
||||
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(
|
||||
/[\{\}\[\]\\\/\<\>\(\)\|\+\?\*\^\'\`\'\"\.\_\$\s~!@#%&,;:=]/gi,
|
||||
"-"
|
||||
)
|
||||
.replace(/\-\-+/g, "-")
|
||||
.replace(/^\-/, "")
|
||||
.replace(/\-$/, "");
|
||||
|
||||
return cleanItem;
|
||||
};
|
||||
|
||||
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, .post-bottom-wrapper a",
|
||||
dtocMobile
|
||||
);
|
||||
|
||||
const onScroll = () => {
|
||||
run.throttle(this, self.highlightItemsOnScroll, self, SCROLL_THROTTLE);
|
||||
};
|
||||
|
||||
$(window).on("scroll.d-toc", onScroll);
|
||||
},
|
||||
|
||||
highlightItemsOnScroll: self => {
|
||||
$("html, body")
|
||||
.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.parents(".d-toc-subheading:has(.d-toc-active)"));
|
||||
|
||||
return target.slideUp(TOC_ANIMATION_SPEED);
|
||||
},
|
||||
|
||||
show: function (elem) {
|
||||
return elem.slideDown(TOC_ANIMATION_SPEED);
|
||||
},
|
||||
|
||||
scrollTo: function (elem) {
|
||||
const currentDiv = $(`[data-d-toc="${elem.attr("data-d-toc")}"]`);
|
||||
|
||||
$("html, body").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>`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
body.append(`<span id="bottom-anchor" class="d-toc-igonore"></span>`);
|
||||
|
||||
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
|
||||
})
|
||||
.addClass("d-toc-post-heading");
|
||||
});
|
||||
|
||||
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(
|
||||
`<div class="d-toc-main">
|
||||
<div class="post-bottom-wrapper dekstop">
|
||||
<a href="#bottom-anchor" title="${I18n.t(
|
||||
themePrefix("post_bottom_tooltip")
|
||||
)}">${iconHTML("downward")}</a>
|
||||
</div>
|
||||
<ul id="d-toc">
|
||||
<div class="d-toc-close-wrapper mobile">
|
||||
<div class="post-bottom-wrapper">
|
||||
<a href="#bottom-anchor" title="${I18n.t(
|
||||
themePrefix("post_bottom_tooltip")
|
||||
)}">${iconHTML("downward")}</a>
|
||||
</div>
|
||||
<div class="d-toc-close">
|
||||
${closeIcon}
|
||||
</div>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
|
||||
.parents(".topic-post")
|
||||
.addClass("d-toc-post")
|
||||
.parents("body")
|
||||
.addClass("d-toc-timeline");
|
||||
|
||||
$("#d-toc").dToc({
|
||||
cooked: body,
|
||||
selectors: dTocHeadingSelectors
|
||||
});
|
||||
});
|
||||
},
|
||||
{ id: "disco-toc" }
|
||||
);
|
||||
|
||||
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", {
|
||||
pluginId: "DiscoTOC",
|
||||
|
||||
actions: {
|
||||
insertDtoc() {
|
||||
this.get("toolbarEvent").applySurround(
|
||||
`<div data-theme-toc="true">`,
|
||||
`</div>`,
|
||||
"contains_dtoc"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,3 @@
|
|||
<a {{action "showTOCOverlay"}} href class="btn btn-primary">
|
||||
{{theme-i18n "table_of_contents"}}
|
||||
</a>
|
|
@ -0,0 +1,7 @@
|
|||
export default {
|
||||
actions: {
|
||||
showTOCOverlay() {
|
||||
document.querySelector(".d-toc-wrapper").classList.toggle("overlay");
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
{{!-- TOC placeholder --}}
|
|
@ -0,0 +1,47 @@
|
|||
import I18n from "I18n";
|
||||
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||
|
||||
export default {
|
||||
name: "disco-toc-composer",
|
||||
|
||||
initialize() {
|
||||
withPluginApi("1.0.0", (api) => {
|
||||
const currentUser = api.getCurrentUser();
|
||||
if (!currentUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const minimumTL = settings.minimum_trust_level_to_create_TOC;
|
||||
|
||||
if (currentUser.trust_level >= minimumTL) {
|
||||
if (!I18n.translations[I18n.currentLocale()].js.composer) {
|
||||
I18n.translations[I18n.currentLocale()].js.composer = {};
|
||||
}
|
||||
I18n.translations[I18n.currentLocale()].js.composer.contains_dtoc = " ";
|
||||
|
||||
api.modifyClass("controller:composer", {
|
||||
pluginId: "DiscoTOC",
|
||||
|
||||
actions: {
|
||||
insertDtoc() {
|
||||
this.get("toolbarEvent").applySurround(
|
||||
`<div data-theme-toc="true">`,
|
||||
`</div>`,
|
||||
"contains_dtoc"
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
api.addToolbarPopupMenuOptionsCallback((controller) => {
|
||||
return {
|
||||
action: "insertDtoc",
|
||||
icon: "align-left",
|
||||
label: themePrefix("insert_table_of_contents"),
|
||||
condition: controller.get("model.creatingTopic"),
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
|
@ -0,0 +1,263 @@
|
|||
import domUtils from "discourse-common/utils/dom-utils";
|
||||
import { headerOffset } from "discourse/lib/offset-calculator";
|
||||
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||
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"]`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let dTocHeadingSelectors =
|
||||
":scope > h1, :scope > h2, :scope > h3, :scope > h4, :scope > h5, :scope > h6";
|
||||
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");
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
const startingLevel =
|
||||
parseInt(headings[0].tagName.substring(1), 10) - 1;
|
||||
let result = document.createElement("div");
|
||||
result.setAttribute("id", "d-toc");
|
||||
buildTOC(headings, result, startingLevel || 1);
|
||||
document.querySelector(".d-toc-main").appendChild(result);
|
||||
document.addEventListener("click", this.clickTOC, false);
|
||||
}
|
||||
},
|
||||
{
|
||||
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");
|
||||
}
|
||||
});
|
||||
|
||||
api.onAppEvent("topic:current-post-scrolled", (args) => {
|
||||
if (args.postIndex !== 1) {
|
||||
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")}"]`
|
||||
);
|
||||
|
||||
anchor.parentElement.classList.add("direct-active");
|
||||
parentsUntil(anchor, "#d-toc", ".d-toc-item").forEach((liParent) => {
|
||||
liParent.classList.add("active");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
api.cleanupStream(() => {
|
||||
document.body.classList.remove("d-toc-timeline-visible");
|
||||
document.removeEventListener("click", this.clickTOC, false);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
clickTOC(e) {
|
||||
if (!document.body.classList.contains("d-toc-timeline-visible")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// link to each heading
|
||||
if (e.target.hasAttribute("data-d-toc")) {
|
||||
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",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// close overlay
|
||||
if (e.target.closest("a").classList.contains("d-toc-close")) {
|
||||
document.querySelector(".d-toc-wrapper").classList.remove("overlay");
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function buildTOC(nodesList, elm, lv = 1) {
|
||||
let nodes = Array.from(nodesList);
|
||||
node = nodes.shift();
|
||||
|
||||
let node;
|
||||
if (node) {
|
||||
let li, cnt;
|
||||
let curLv = parseInt(node.tagName.substring(1), 10);
|
||||
|
||||
if (curLv === lv) {
|
||||
// same level
|
||||
cnt = 0;
|
||||
} else if (curLv < lv) {
|
||||
// walk up then append
|
||||
cnt = 0;
|
||||
do {
|
||||
elm = elm.parentNode.parentNode;
|
||||
cnt--;
|
||||
} while (cnt > curLv - lv);
|
||||
} else if (curLv > lv) {
|
||||
// add children
|
||||
cnt = 0;
|
||||
do {
|
||||
li = elm.lastChild;
|
||||
if (li == null) {
|
||||
elm = elm.appendChild(document.createElement("ul"));
|
||||
} else {
|
||||
elm = li.appendChild(document.createElement("ul"));
|
||||
}
|
||||
cnt++;
|
||||
} while (cnt < curLv - lv);
|
||||
}
|
||||
if (curLv === 1 && elm.lastChild === null) {
|
||||
elm = elm.appendChild(document.createElement("ul"));
|
||||
}
|
||||
// append list item
|
||||
|
||||
li = elm.appendChild(document.createElement("li"));
|
||||
li.classList.add("d-toc-item");
|
||||
li.innerHTML = `<a data-d-toc="${node.getAttribute("id")}">${
|
||||
node.textContent
|
||||
}</a>`;
|
||||
|
||||
// recurse
|
||||
buildTOC(nodes, elm, lv + cnt);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "DiscoTOC",
|
||||
"version": "2.0.0",
|
||||
"repository": "https://github.com/discourse/DiscoTOC",
|
||||
"author": "Discourse",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"eslint-config-discourse": "^1.1.9"
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
.d-toc-timeline .topic-chat-float-container {
|
||||
z-index: z("header");
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
.discourse-sidebar .d-toc-regular .d-toc-article .row {
|
||||
width: 75%;
|
||||
@media screen and (max-width: calc($large-width + 100px)) {
|
||||
width: calc(100vw - 240px - 3em);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: calc($large-width + 100px)) {
|
||||
.discourse-sidebar {
|
||||
.d-toc-regular {
|
||||
.post-bottom-wrapper {
|
||||
padding: 1em 0.75em;
|
||||
&.desktop {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
#d-toc {
|
||||
z-index: z("header") + 1;
|
||||
background: var(--secondary);
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
max-width: 500px;
|
||||
overflow: scroll;
|
||||
transition: transform 0.5s, opacity 0.25s;
|
||||
transform: translatex(100%);
|
||||
opacity: 0;
|
||||
:not(.rtl) & {
|
||||
margin-left: -1px;
|
||||
}
|
||||
.rtl & {
|
||||
margin-right: -1px;
|
||||
}
|
||||
&.d-toc-mobile {
|
||||
transform: translatex(0);
|
||||
opacity: 1;
|
||||
}
|
||||
.d-toc-active {
|
||||
&:before {
|
||||
:not(.rtl) & {
|
||||
margin-left: -1px;
|
||||
}
|
||||
.rtl & {
|
||||
margin-right: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.d-toc-close-wrapper {
|
||||
height: 3em;
|
||||
background: var(--secondary);
|
||||
color: var(--primary-med-or-secondary-med);
|
||||
margin-bottom: 1em;
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
.d-toc-close {
|
||||
padding: 1em 0.75em;
|
||||
}
|
||||
}
|
||||
.d-toc-toggle {
|
||||
position: fixed;
|
||||
bottom: 5px;
|
||||
|
||||
padding: 0.5em 1em;
|
||||
background: var(--tertiary);
|
||||
color: var(--secondary);
|
||||
z-index: 3;
|
||||
margin-bottom: env(safe-area-inset-bottom);
|
||||
:not(.rtl) & {
|
||||
right: 16px;
|
||||
}
|
||||
.rtl & {
|
||||
left: 16px;
|
||||
}
|
||||
body.footer-nav-visible & {
|
||||
bottom: 49px;
|
||||
}
|
||||
}
|
||||
#d-toc > ul {
|
||||
:not(.rtl) & {
|
||||
margin-left: 20px;
|
||||
border-left: 1px solid var(--primary-low);
|
||||
}
|
||||
.rtl & {
|
||||
margin-right: 20px;
|
||||
border-right: 1px solid var(--primary-low);
|
||||
}
|
||||
&:last-child {
|
||||
margin-bottom: 5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-view .discourse-sidebar .d-toc-regular .d-toc-article .row {
|
||||
width: 100%;
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { acceptance, query } from "discourse/tests/helpers/qunit-helpers";
|
||||
import { click, visit } from "@ember/test-helpers";
|
||||
import selectKit from "discourse/tests/helpers/select-kit-helper";
|
||||
import { test } from "qunit";
|
||||
|
||||
acceptance("DiscoTOC - Composer", function (needs) {
|
||||
needs.user();
|
||||
|
||||
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.selectRowByValue("insertDtoc");
|
||||
|
||||
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());
|
||||
});
|
||||
});
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue