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
|
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";
|
$padding-basis: 0.75em;
|
||||||
@import "sidebar";
|
|
||||||
@import "chat";
|
|
||||||
|
|
||||||
.d-toc-regular {
|
.d-toc-main {
|
||||||
[data-theme-toc] {
|
display: none;
|
||||||
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 {
|
#d-toc {
|
||||||
font-size: $font-up-1;
|
max-height: calc(100vh - 3em - var(--header-offset));
|
||||||
margin: 0 0 10px 0;
|
overflow: auto;
|
||||||
font-weight: bold;
|
ul {
|
||||||
}
|
list-style-type: none;
|
||||||
.highlighted {
|
|
||||||
animation: fadein 1s;
|
|
||||||
}
|
|
||||||
.d-toc {
|
|
||||||
transform: translate3d(0, 0, 0);
|
|
||||||
transition: opacity 0.25s;
|
|
||||||
ul,
|
|
||||||
li {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: none;
|
|
||||||
}
|
}
|
||||||
.d-toc-item {
|
li.d-toc-item {
|
||||||
padding: 6px 0;
|
margin: 0;
|
||||||
a {
|
padding: 0;
|
||||||
color: var(--primary-high);
|
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;
|
position: relative;
|
||||||
|
color: var(--primary);
|
||||||
&:before {
|
&:before {
|
||||||
:not(.rtl) & {
|
|
||||||
border-left: 1px solid var(--tertiary);
|
|
||||||
}
|
|
||||||
.rtl & {
|
|
||||||
border-right: 1px solid var(--tertiary);
|
|
||||||
}
|
|
||||||
height: 100%;
|
|
||||||
content: "";
|
content: "";
|
||||||
width: 1px;
|
width: 1px;
|
||||||
|
margin-top: -1px;
|
||||||
|
background-color: var(--tertiary);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
height: 100%;
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: var(--primary);
|
|
||||||
text-shadow: 0.1px 0.1px var(--primary),
|
|
||||||
-0.1px -0.1px var(--primary);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.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 {
|
> ul > li > ul {
|
||||||
a {
|
font-size: var(--font-down-1);
|
||||||
color: var(--primary-med-or-secondary-med);
|
li.direct-active > a:before {
|
||||||
|
// it's odd that we need this
|
||||||
|
margin-left: -1px;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// large screens
|
// active line marker
|
||||||
@media screen and (min-width: $large-width) {
|
$selector: "> ul > li.direct-active > a:before";
|
||||||
.d-toc {
|
$sub: "> ul > li";
|
||||||
margin-top: 1em;
|
$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;
|
// END active line marker
|
||||||
&.mobile {
|
|
||||||
display: none;
|
.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 {
|
// core overrides when timeline is active
|
||||||
display: none !important;
|
.timeline-container,
|
||||||
}
|
#topic-progress {
|
||||||
.d-toc-close-wrapper,
|
|
||||||
.d-toc-subheading {
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.d-toc-post {
|
.container.posts .topic-navigation.with-topic-progress {
|
||||||
.topic-body,
|
align-self: start;
|
||||||
.topic-avatar {
|
}
|
||||||
border-top: none;
|
}
|
||||||
}
|
|
||||||
.d-toc {
|
// RTL Support
|
||||||
max-height: 85vh;
|
.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;
|
padding-left: 0;
|
||||||
position: -webkit-sticky;
|
padding-right: $padding-basis;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.d-toc-article {
|
}
|
||||||
display: flex;
|
|
||||||
.post-notice {
|
.d-toc-timeline-visible .topic-navigation.with-topic-progress .d-toc-wrapper {
|
||||||
display: none;
|
right: unset;
|
||||||
}
|
left: -100vw;
|
||||||
.topic-map {
|
&.overlay {
|
||||||
margin-bottom: 0;
|
right: unset;
|
||||||
}
|
left: 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Composer preview notice
|
||||||
.edit-title .d-editor-preview [data-theme-toc] {
|
.edit-title .d-editor-preview [data-theme-toc] {
|
||||||
background: var(--tertiary);
|
background: var(--tertiary);
|
||||||
color: var(--secondary);
|
color: var(--secondary);
|
||||||
border-top: 2px solid var(--secondary);
|
border-top: 2px solid var(--secondary);
|
||||||
position: -webkit-sticky;
|
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
height: 30px;
|
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