erudit 3.0.0-dev.15 → 3.0.0-dev.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/app.vue +191 -195
- package/app/components/Loading.vue +23 -23
- package/app/components/SiteAside.vue +393 -393
- package/app/components/SiteMain.vue +32 -32
- package/app/components/ads/Ads.vue +35 -0
- package/app/components/ads/AdsBannerAside.vue +61 -0
- package/app/components/ads/AdsBannerBottom.vue +22 -0
- package/app/components/ads/AdsProviderCustom.vue +35 -0
- package/app/components/ads/AdsProviderYandex.vue +54 -0
- package/app/components/ads/AdsReplacer.vue +73 -0
- package/app/components/aside/AsideListItem.vue +74 -74
- package/app/components/aside/AsideMajor.vue +56 -56
- package/app/components/aside/AsideMinor.vue +73 -71
- package/app/components/aside/major/PaneContentScroll.vue +23 -23
- package/app/components/aside/major/PaneSwitch.vue +54 -54
- package/app/components/aside/major/PaneSwitchButton.vue +63 -63
- package/app/components/aside/major/SiteInfo.vue +85 -85
- package/app/components/aside/major/panes/Language.vue +79 -79
- package/app/components/aside/major/panes/Pages.vue +34 -34
- package/app/components/aside/major/panes/Search.vue +11 -11
- package/app/components/aside/major/panes/nav/Nav.vue +92 -92
- package/app/components/aside/major/panes/nav/NavBook.vue +95 -95
- package/app/components/aside/major/panes/nav/NavBookLoading.vue +24 -24
- package/app/components/aside/major/panes/nav/NavGlobal.vue +16 -16
- package/app/components/aside/major/panes/nav/fnav/FNav.vue +105 -105
- package/app/components/aside/major/panes/nav/fnav/FNavBook.vue +32 -32
- package/app/components/aside/major/panes/nav/fnav/FNavFlags.vue +40 -40
- package/app/components/aside/major/panes/nav/fnav/FNavFolder.vue +60 -60
- package/app/components/aside/major/panes/nav/fnav/FNavItem.vue +34 -34
- package/app/components/aside/major/panes/nav/fnav/FNavSeparator.vue +80 -80
- package/app/components/aside/major/panes/nav/fnav/FNavTopic.vue +24 -24
- package/app/components/aside/major/panes/other/ItemContent.vue +29 -29
- package/app/components/aside/major/panes/other/ItemGenerator.vue +13 -13
- package/app/components/aside/major/panes/other/ItemTheme.vue +54 -54
- package/app/components/aside/major/panes/other/Other.vue +16 -16
- package/app/components/aside/minor/AsideMinorContributor.vue +5 -5
- package/app/components/aside/minor/AsideMinorNews.vue +11 -11
- package/app/components/aside/minor/AsideMinorPane.vue +15 -15
- package/app/components/aside/minor/AsideMinorTopLink.vue +67 -67
- package/app/components/aside/minor/Contribute.vue +145 -145
- package/app/components/aside/minor/content/AsideMinorContent.vue +92 -92
- package/app/components/aside/minor/topic/AsideMinorTopic.vue +32 -32
- package/app/components/aside/minor/topic/TopicContributors.vue +177 -177
- package/app/components/aside/minor/topic/TopicNav.vue +49 -49
- package/app/components/aside/minor/topic/TopicToc.vue +214 -214
- package/app/components/aside/minor/topic/TopicTocItem.vue +32 -32
- package/app/components/aside/utils/AsideOverlayPane.vue +40 -40
- package/app/components/bitran/BitranContent.vue +91 -91
- package/app/components/bitran/RenderWrapper.vue +10 -10
- package/app/components/contributor/ContributorAvatar.vue +45 -45
- package/app/components/contributor/ContributorListItem.vue +35 -35
- package/app/components/main/topic/MainTopic.vue +88 -78
- package/app/components/main/topic/TopicPartSwitch.vue +118 -118
- package/app/components/main/utils/Breadcrumb.vue +74 -70
- package/app/components/main/utils/ContentDecoration.vue +29 -29
- package/app/components/main/utils/ContentDescription.vue +19 -19
- package/app/components/main/utils/ContentPopover.vue +188 -188
- package/app/components/main/utils/ContentPopovers.vue +111 -111
- package/app/components/main/utils/ContentReferences.vue +70 -70
- package/app/components/main/utils/ContentSection.vue +45 -45
- package/app/components/main/utils/ContentTitle.vue +63 -63
- package/app/components/main/utils/reference/ReferenceGroup.vue +38 -38
- package/app/components/main/utils/reference/ReferenceItem.vue +70 -70
- package/app/components/main/utils/reference/ReferenceSource.vue +120 -118
- package/app/components/preview/Preview.vue +186 -186
- package/app/components/preview/PreviewDisplay.vue +139 -139
- package/app/components/preview/PreviewFooterAction.vue +73 -73
- package/app/components/preview/PreviewLoading.vue +14 -14
- package/app/components/preview/PreviewScreen.vue +141 -141
- package/app/components/preview/display/Alert.vue +50 -50
- package/app/components/preview/display/Custom.vue +18 -18
- package/app/components/preview/display/GenericLink.vue +48 -48
- package/app/components/preview/display/PageLink.vue +22 -20
- package/app/components/preview/display/Unique.vue +55 -55
- package/app/components/transition/Fade.vue +19 -19
- package/app/components/tree/TreeContainer.vue +11 -11
- package/app/components/tree/TreeItem.vue +89 -89
- package/app/composables/bitran.ts +108 -108
- package/app/composables/bitranContent.ts +92 -92
- package/app/composables/bitranLocation.ts +7 -7
- package/app/composables/contentData.ts +38 -38
- package/app/composables/contentPage.ts +158 -158
- package/app/composables/contentRoute.ts +45 -45
- package/app/composables/darkMagic.ts +24 -24
- package/app/composables/externalApi.ts +69 -63
- package/app/composables/favicon.ts +8 -8
- package/app/composables/formatText.ts +99 -99
- package/app/composables/majorPane.ts +60 -60
- package/app/composables/phrases.ts +65 -65
- package/app/composables/theme.ts +29 -29
- package/app/composables/url.ts +33 -33
- package/app/pages/_test/preview.vue +110 -110
- package/app/pages/article/[...articleId].vue +3 -3
- package/app/pages/book/[...bookId].vue +47 -47
- package/app/pages/group/[...groupId].vue +66 -64
- package/app/pages/index.vue +32 -32
- package/app/pages/members.vue +6 -6
- package/app/pages/practice/[...practice].vue +3 -3
- package/app/pages/summary/[...summaryId].vue +3 -3
- package/app/plugins/analytics.ts +87 -0
- package/app/plugins/prerender.server.ts +22 -22
- package/app/scripts/_immediate.js +9 -9
- package/app/scripts/aside/index.ts +59 -59
- package/app/scripts/aside/major/nav.ts +26 -26
- package/app/scripts/aside/minor/state.ts +37 -37
- package/app/scripts/aside/minor/topic.ts +3 -3
- package/app/scripts/flag.ts +28 -28
- package/app/scripts/og.ts +27 -27
- package/app/scripts/preview/build.ts +76 -76
- package/app/scripts/preview/data/alert.ts +19 -19
- package/app/scripts/preview/data/custom.ts +8 -8
- package/app/scripts/preview/data/genericLink.ts +24 -24
- package/app/scripts/preview/data/pageLink.ts +23 -23
- package/app/scripts/preview/data/unique.ts +72 -72
- package/app/scripts/preview/data.ts +24 -24
- package/app/scripts/preview/display.ts +37 -37
- package/app/scripts/preview/footer.ts +9 -9
- package/app/scripts/preview/request.ts +51 -51
- package/app/scripts/preview/state.ts +63 -63
- package/app/styles/_immediate.css +7 -7
- package/app/styles/_util.scss +43 -43
- package/app/styles/_utils.scss +44 -44
- package/app/styles/app.scss +91 -91
- package/app/styles/def/_bp.scss +27 -27
- package/app/styles/def/_size.scss +7 -7
- package/app/styles/def/_z.scss +5 -5
- package/app/styles/normalize.scss +49 -49
- package/app/styles/partials/_darkMagic.scss +5 -5
- package/app/styles/partials/_fnav.scss +15 -15
- package/app/styles/partials/_preview.scss +5 -5
- package/bin/erudit.mjs +2 -2
- package/const.ts +4 -4
- package/globalPath.ts +21 -21
- package/globals/bitran.ts +1 -1
- package/globals/content.ts +27 -27
- package/globals/contributor.ts +5 -5
- package/globals/erudit.ts +5 -5
- package/globals/register.ts +18 -18
- package/languages/en.ts +94 -94
- package/languages/ru.ts +98 -98
- package/module/bitran.ts +66 -66
- package/module/config.ts +35 -35
- package/module/imports.ts +67 -67
- package/module/index.ts +47 -49
- package/module/logger.ts +10 -10
- package/module/paths.ts +22 -22
- package/module/restart.ts +61 -61
- package/nuxt.config.ts +131 -134
- package/package.json +8 -7
- package/server/api/aside/major/nav/bookIds.ts +5 -5
- package/server/api/aside/major/nav/bookNav/[...bookId].ts +20 -20
- package/server/api/aside/major/nav/global.ts +7 -7
- package/server/api/aside/minor/news.ts +7 -7
- package/server/api/aside/minor/path.ts +82 -82
- package/server/api/bitran/content/[...location].ts +10 -10
- package/server/api/bitran/toc/[...location].ts +9 -9
- package/server/api/content/data.ts +75 -75
- package/server/api/contributor/count.ts +6 -6
- package/server/api/fake/content.ts +11 -11
- package/server/api/fake/shared/languages.ts +12 -12
- package/server/api/language/functions.ts +12 -12
- package/server/api/language/phrase/[phraseId].ts +19 -19
- package/server/api/language/phraseIds.ts +8 -8
- package/server/api/prerender.ts +120 -120
- package/server/api/preview/page/[...parts].ts +78 -78
- package/server/api/preview/unique/[...location].ts +61 -61
- package/server/api/problem/generator/[...path].ts +26 -0
- package/server/plugin/bitran/content.ts +190 -190
- package/server/plugin/bitran/elements/include.ts +229 -229
- package/server/plugin/bitran/location.ts +39 -39
- package/server/plugin/bitran/toc.ts +94 -94
- package/server/plugin/bitran/transpiler.ts +18 -18
- package/server/plugin/build/close.ts +12 -12
- package/server/plugin/build/jobs/content/builderArgs.ts +8 -8
- package/server/plugin/build/jobs/content/generic.ts +191 -191
- package/server/plugin/build/jobs/content/parse.ts +113 -113
- package/server/plugin/build/jobs/content/path.ts +6 -6
- package/server/plugin/build/jobs/content/type/book.ts +9 -9
- package/server/plugin/build/jobs/content/type/group.ts +37 -37
- package/server/plugin/build/jobs/content/type/topic.ts +36 -36
- package/server/plugin/build/jobs/contributors.ts +66 -66
- package/server/plugin/build/jobs/language.ts +36 -36
- package/server/plugin/build/jobs/nav.ts +345 -265
- package/server/plugin/build/process.ts +32 -32
- package/server/plugin/build/rebuild.ts +66 -66
- package/server/plugin/build/setup.ts +19 -19
- package/server/plugin/content/context.ts +119 -119
- package/server/plugin/db/entities/Book.ts +7 -7
- package/server/plugin/db/entities/Content.ts +45 -45
- package/server/plugin/db/entities/Contribution.ts +10 -10
- package/server/plugin/db/entities/Contributor.ts +16 -16
- package/server/plugin/db/entities/File.ts +10 -10
- package/server/plugin/db/entities/Group.ts +14 -14
- package/server/plugin/db/entities/Hash.ts +15 -15
- package/server/plugin/db/entities/Topic.ts +20 -20
- package/server/plugin/db/entities/Unique.ts +21 -21
- package/server/plugin/db/reset.ts +12 -12
- package/server/plugin/db/setup.ts +49 -49
- package/server/plugin/global.ts +16 -16
- package/server/plugin/importer.ts +16 -16
- package/server/plugin/index.ts +9 -9
- package/server/plugin/logger.ts +23 -23
- package/server/plugin/nav/node.ts +27 -27
- package/server/plugin/nav/utils.ts +175 -175
- package/server/plugin/repository/book.ts +21 -21
- package/server/plugin/repository/content.ts +240 -240
- package/server/plugin/repository/contentId.ts +40 -40
- package/server/plugin/repository/contributor.ts +8 -8
- package/server/plugin/repository/file.ts +10 -10
- package/server/plugin/repository/frontNav.ts +145 -145
- package/server/plugin/repository/topic.ts +35 -35
- package/server/tsconfig.json +9 -9
- package/shared/aside/minor.ts +51 -51
- package/shared/asset.ts +22 -22
- package/shared/bitran/contentId.ts +56 -56
- package/shared/bitran/stringContent.ts +6 -6
- package/shared/bitran/toc.ts +8 -8
- package/shared/content/bookId.ts +12 -12
- package/shared/content/context.ts +9 -9
- package/shared/content/data/base.ts +32 -32
- package/shared/content/data/index.ts +5 -5
- package/shared/content/data/type/book.ts +5 -5
- package/shared/content/data/type/group.ts +6 -6
- package/shared/content/data/type/topic.ts +11 -11
- package/shared/content/previousNext.ts +9 -9
- package/shared/contributor.ts +5 -5
- package/shared/frontNav.ts +41 -41
- package/shared/icons.ts +38 -38
- package/shared/image.ts +5 -5
- package/shared/link.ts +28 -28
- package/shared/popover.ts +8 -8
- package/shared/types/language.ts +74 -74
- package/shared/utils/objectsEqual.ts +4 -4
- package/shared/utils/stringColor.ts +9 -9
- package/test/contentId.test.ts +91 -91
- package/tsconfig.json +8 -8
- package/utils/contentPath.ts +67 -67
- package/utils/slash.ts +11 -11
- package/utils/stress.ts +9 -9
- package/app/components/ads/BannerTemplate.vue +0 -51
- package/app/components/ads/BottomBanner.vue +0 -45
- package/app/components/ads/LeftBanner.vue +0 -50
- package/module/problemGenerators.ts +0 -46
|
@@ -1,214 +1,214 @@
|
|
|
1
|
-
<script lang="ts" setup>
|
|
2
|
-
import { headingName } from '@erudit-js/bitran-elements/heading/shared';
|
|
3
|
-
|
|
4
|
-
import { stringifyBitranLocation } from '@erudit-js/cog/schema';
|
|
5
|
-
import type { TocItem } from '@erudit/shared/bitran/toc';
|
|
6
|
-
import { topicLocation } from '@app/scripts/aside/minor/topic';
|
|
7
|
-
import { injectAsideData } from '@app/scripts/aside/minor/state';
|
|
8
|
-
import type { AsideMinorTopic } from '@shared/aside/minor';
|
|
9
|
-
|
|
10
|
-
import TopicTocItem from './TopicTocItem.vue';
|
|
11
|
-
|
|
12
|
-
interface RuntimeTocItem extends TocItem {
|
|
13
|
-
/**
|
|
14
|
-
* * `0` — Not active
|
|
15
|
-
* * `1` — Active for `window` events
|
|
16
|
-
* * `2` — Active for Intersection Observer and currently in viewport
|
|
17
|
-
*/
|
|
18
|
-
_active: 0 | 1 | 2;
|
|
19
|
-
_position: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
type RuntimeToc = RuntimeTocItem[];
|
|
23
|
-
|
|
24
|
-
const topicData = injectAsideData<AsideMinorTopic>();
|
|
25
|
-
const phrase = await usePhrases('empty_toc');
|
|
26
|
-
const runtimeToc = ref<RuntimeToc>([]);
|
|
27
|
-
const tocStateKey = ref(0);
|
|
28
|
-
|
|
29
|
-
watch(topicData, setupRuntimeToc);
|
|
30
|
-
setupRuntimeToc();
|
|
31
|
-
|
|
32
|
-
function setupRuntimeToc(): void {
|
|
33
|
-
const _newToc: RuntimeToc = [];
|
|
34
|
-
|
|
35
|
-
for (let i = 0; i < topicData.value.toc.length; i++) {
|
|
36
|
-
_newToc.push({
|
|
37
|
-
...topicData.value.toc[i]!,
|
|
38
|
-
_active: 0,
|
|
39
|
-
_position: i,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
runtimeToc.value = _newToc;
|
|
44
|
-
tocStateKey.value++;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
//
|
|
48
|
-
// Live TOC
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
let observer: IntersectionObserver | null = null;
|
|
52
|
-
let id2TocItemIndex: Record<string, number> = {};
|
|
53
|
-
|
|
54
|
-
const windowEvents = ['DOMContentLoaded', 'load', 'resize', 'scroll'] as const;
|
|
55
|
-
let headings: RuntimeTocItem[] = [];
|
|
56
|
-
let closestAboveHeading: RuntimeTocItem | null = null;
|
|
57
|
-
|
|
58
|
-
function disableLiveToc(): void {
|
|
59
|
-
// Skip if not in client-side
|
|
60
|
-
if (import.meta.server) return;
|
|
61
|
-
|
|
62
|
-
// Live TOC heading with `window` events
|
|
63
|
-
for (const event of windowEvents) {
|
|
64
|
-
window.removeEventListener(event, updateActiveTopHeading);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
headings = [];
|
|
68
|
-
closestAboveHeading = null;
|
|
69
|
-
|
|
70
|
-
// Live TOC with Intersection Observer
|
|
71
|
-
id2TocItemIndex = {};
|
|
72
|
-
|
|
73
|
-
if (observer) {
|
|
74
|
-
observer.disconnect();
|
|
75
|
-
observer = null;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Reset active state
|
|
79
|
-
if (runtimeToc.value?.length) {
|
|
80
|
-
for (const tocItem of runtimeToc.value) {
|
|
81
|
-
tocItem._active = 0;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function enableLiveToc(): void {
|
|
87
|
-
// Skip if not in client-side
|
|
88
|
-
if (import.meta.server) return;
|
|
89
|
-
if (!runtimeToc.value?.length) return;
|
|
90
|
-
|
|
91
|
-
// Live TOC heading with `window` events
|
|
92
|
-
headings = runtimeToc.value.filter(
|
|
93
|
-
(item) => item.productName === headingName,
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
for (const event of windowEvents) {
|
|
97
|
-
window.addEventListener(event, updateActiveTopHeading);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
updateActiveTopHeading();
|
|
101
|
-
|
|
102
|
-
// Live TOC with Intersection Observer
|
|
103
|
-
observer = new IntersectionObserver(intersectionTrigger);
|
|
104
|
-
id2TocItemIndex = {};
|
|
105
|
-
|
|
106
|
-
for (const tocItem of runtimeToc.value) {
|
|
107
|
-
const id = tocItem.id;
|
|
108
|
-
id2TocItemIndex[id] = tocItem._position;
|
|
109
|
-
const element = document.getElementById(id);
|
|
110
|
-
if (element) {
|
|
111
|
-
observer.observe(element);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function updateActiveTopHeading(): void {
|
|
117
|
-
function getBottom(id: string): number {
|
|
118
|
-
const defaultBottom = 1;
|
|
119
|
-
const element = document.getElementById(id);
|
|
120
|
-
return element ? element.getBoundingClientRect().bottom : defaultBottom;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (closestAboveHeading) {
|
|
124
|
-
closestAboveHeading._active = closestAboveHeading._active === 2 ? 2 : 0;
|
|
125
|
-
closestAboveHeading = null;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (!runtimeToc.value?.length || !headings.length) return;
|
|
129
|
-
|
|
130
|
-
let topIndex = 0;
|
|
131
|
-
let bottomIndex = headings.length;
|
|
132
|
-
let targetIndex = 0;
|
|
133
|
-
|
|
134
|
-
while (topIndex < bottomIndex) {
|
|
135
|
-
const middleIndex = ((topIndex + bottomIndex) / 2) | 0;
|
|
136
|
-
const middleHeading = headings[middleIndex]!;
|
|
137
|
-
const middleHeadingTop = getBottom(middleHeading.id);
|
|
138
|
-
|
|
139
|
-
if (middleHeadingTop <= 0) {
|
|
140
|
-
targetIndex = middleIndex;
|
|
141
|
-
topIndex = middleIndex + 1;
|
|
142
|
-
} else {
|
|
143
|
-
bottomIndex = middleIndex;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
closestAboveHeading = headings[targetIndex]!;
|
|
148
|
-
if (closestAboveHeading && closestAboveHeading._active < 2) {
|
|
149
|
-
closestAboveHeading._active =
|
|
150
|
-
getBottom(closestAboveHeading.id) <= 0 ? 1 : 0;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function intersectionTrigger(entries: IntersectionObserverEntry[]): void {
|
|
155
|
-
for (const entry of entries) {
|
|
156
|
-
const itemIndex = id2TocItemIndex[entry.target.id]!;
|
|
157
|
-
const tocItem = runtimeToc.value?.[itemIndex];
|
|
158
|
-
|
|
159
|
-
if (tocItem) {
|
|
160
|
-
tocItem._active = entry.isIntersecting
|
|
161
|
-
? 2
|
|
162
|
-
: tocItem._active === 1
|
|
163
|
-
? 1
|
|
164
|
-
: 0;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
onMounted(() => {
|
|
170
|
-
watch(
|
|
171
|
-
[topicData, topicLocation],
|
|
172
|
-
() => {
|
|
173
|
-
disableLiveToc();
|
|
174
|
-
|
|
175
|
-
if (!topicData.value.location || !topicLocation.value) return;
|
|
176
|
-
|
|
177
|
-
enableLiveToc();
|
|
178
|
-
},
|
|
179
|
-
{ immediate: true },
|
|
180
|
-
);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
onUnmounted(() => {
|
|
184
|
-
disableLiveToc();
|
|
185
|
-
});
|
|
186
|
-
</script>
|
|
187
|
-
|
|
188
|
-
<template>
|
|
189
|
-
<section :class="$style.topicToc">
|
|
190
|
-
<TreeContainer v-if="runtimeToc?.length > 0" :key="tocStateKey">
|
|
191
|
-
<TopicTocItem
|
|
192
|
-
v-for="tocItem of runtimeToc"
|
|
193
|
-
v-memo="[tocStateKey, tocItem.id, tocItem._active]"
|
|
194
|
-
:active="!!tocItem._active"
|
|
195
|
-
:tocItem
|
|
196
|
-
/>
|
|
197
|
-
</TreeContainer>
|
|
198
|
-
<div v-else :class="$style.tocEmpty">{{ phrase.empty_toc }}</div>
|
|
199
|
-
</section>
|
|
200
|
-
</template>
|
|
201
|
-
|
|
202
|
-
<style lang="scss" module>
|
|
203
|
-
.topicToc {
|
|
204
|
-
flex: 1;
|
|
205
|
-
overflow: auto;
|
|
206
|
-
@include scroll();
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
.tocEmpty {
|
|
210
|
-
padding: var(--gap);
|
|
211
|
-
text-align: center;
|
|
212
|
-
color: var(--textMuted);
|
|
213
|
-
}
|
|
214
|
-
</style>
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { headingName } from '@erudit-js/bitran-elements/heading/shared';
|
|
3
|
+
|
|
4
|
+
import { stringifyBitranLocation } from '@erudit-js/cog/schema';
|
|
5
|
+
import type { TocItem } from '@erudit/shared/bitran/toc';
|
|
6
|
+
import { topicLocation } from '@app/scripts/aside/minor/topic';
|
|
7
|
+
import { injectAsideData } from '@app/scripts/aside/minor/state';
|
|
8
|
+
import type { AsideMinorTopic } from '@shared/aside/minor';
|
|
9
|
+
|
|
10
|
+
import TopicTocItem from './TopicTocItem.vue';
|
|
11
|
+
|
|
12
|
+
interface RuntimeTocItem extends TocItem {
|
|
13
|
+
/**
|
|
14
|
+
* * `0` — Not active
|
|
15
|
+
* * `1` — Active for `window` events
|
|
16
|
+
* * `2` — Active for Intersection Observer and currently in viewport
|
|
17
|
+
*/
|
|
18
|
+
_active: 0 | 1 | 2;
|
|
19
|
+
_position: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type RuntimeToc = RuntimeTocItem[];
|
|
23
|
+
|
|
24
|
+
const topicData = injectAsideData<AsideMinorTopic>();
|
|
25
|
+
const phrase = await usePhrases('empty_toc');
|
|
26
|
+
const runtimeToc = ref<RuntimeToc>([]);
|
|
27
|
+
const tocStateKey = ref(0);
|
|
28
|
+
|
|
29
|
+
watch(topicData, setupRuntimeToc);
|
|
30
|
+
setupRuntimeToc();
|
|
31
|
+
|
|
32
|
+
function setupRuntimeToc(): void {
|
|
33
|
+
const _newToc: RuntimeToc = [];
|
|
34
|
+
|
|
35
|
+
for (let i = 0; i < topicData.value.toc.length; i++) {
|
|
36
|
+
_newToc.push({
|
|
37
|
+
...topicData.value.toc[i]!,
|
|
38
|
+
_active: 0,
|
|
39
|
+
_position: i,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
runtimeToc.value = _newToc;
|
|
44
|
+
tocStateKey.value++;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
//
|
|
48
|
+
// Live TOC
|
|
49
|
+
//
|
|
50
|
+
|
|
51
|
+
let observer: IntersectionObserver | null = null;
|
|
52
|
+
let id2TocItemIndex: Record<string, number> = {};
|
|
53
|
+
|
|
54
|
+
const windowEvents = ['DOMContentLoaded', 'load', 'resize', 'scroll'] as const;
|
|
55
|
+
let headings: RuntimeTocItem[] = [];
|
|
56
|
+
let closestAboveHeading: RuntimeTocItem | null = null;
|
|
57
|
+
|
|
58
|
+
function disableLiveToc(): void {
|
|
59
|
+
// Skip if not in client-side
|
|
60
|
+
if (import.meta.server) return;
|
|
61
|
+
|
|
62
|
+
// Live TOC heading with `window` events
|
|
63
|
+
for (const event of windowEvents) {
|
|
64
|
+
window.removeEventListener(event, updateActiveTopHeading);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
headings = [];
|
|
68
|
+
closestAboveHeading = null;
|
|
69
|
+
|
|
70
|
+
// Live TOC with Intersection Observer
|
|
71
|
+
id2TocItemIndex = {};
|
|
72
|
+
|
|
73
|
+
if (observer) {
|
|
74
|
+
observer.disconnect();
|
|
75
|
+
observer = null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Reset active state
|
|
79
|
+
if (runtimeToc.value?.length) {
|
|
80
|
+
for (const tocItem of runtimeToc.value) {
|
|
81
|
+
tocItem._active = 0;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function enableLiveToc(): void {
|
|
87
|
+
// Skip if not in client-side
|
|
88
|
+
if (import.meta.server) return;
|
|
89
|
+
if (!runtimeToc.value?.length) return;
|
|
90
|
+
|
|
91
|
+
// Live TOC heading with `window` events
|
|
92
|
+
headings = runtimeToc.value.filter(
|
|
93
|
+
(item) => item.productName === headingName,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
for (const event of windowEvents) {
|
|
97
|
+
window.addEventListener(event, updateActiveTopHeading);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
updateActiveTopHeading();
|
|
101
|
+
|
|
102
|
+
// Live TOC with Intersection Observer
|
|
103
|
+
observer = new IntersectionObserver(intersectionTrigger);
|
|
104
|
+
id2TocItemIndex = {};
|
|
105
|
+
|
|
106
|
+
for (const tocItem of runtimeToc.value) {
|
|
107
|
+
const id = tocItem.id;
|
|
108
|
+
id2TocItemIndex[id] = tocItem._position;
|
|
109
|
+
const element = document.getElementById(id);
|
|
110
|
+
if (element) {
|
|
111
|
+
observer.observe(element);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function updateActiveTopHeading(): void {
|
|
117
|
+
function getBottom(id: string): number {
|
|
118
|
+
const defaultBottom = 1;
|
|
119
|
+
const element = document.getElementById(id);
|
|
120
|
+
return element ? element.getBoundingClientRect().bottom : defaultBottom;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (closestAboveHeading) {
|
|
124
|
+
closestAboveHeading._active = closestAboveHeading._active === 2 ? 2 : 0;
|
|
125
|
+
closestAboveHeading = null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!runtimeToc.value?.length || !headings.length) return;
|
|
129
|
+
|
|
130
|
+
let topIndex = 0;
|
|
131
|
+
let bottomIndex = headings.length;
|
|
132
|
+
let targetIndex = 0;
|
|
133
|
+
|
|
134
|
+
while (topIndex < bottomIndex) {
|
|
135
|
+
const middleIndex = ((topIndex + bottomIndex) / 2) | 0;
|
|
136
|
+
const middleHeading = headings[middleIndex]!;
|
|
137
|
+
const middleHeadingTop = getBottom(middleHeading.id);
|
|
138
|
+
|
|
139
|
+
if (middleHeadingTop <= 0) {
|
|
140
|
+
targetIndex = middleIndex;
|
|
141
|
+
topIndex = middleIndex + 1;
|
|
142
|
+
} else {
|
|
143
|
+
bottomIndex = middleIndex;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
closestAboveHeading = headings[targetIndex]!;
|
|
148
|
+
if (closestAboveHeading && closestAboveHeading._active < 2) {
|
|
149
|
+
closestAboveHeading._active =
|
|
150
|
+
getBottom(closestAboveHeading.id) <= 0 ? 1 : 0;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function intersectionTrigger(entries: IntersectionObserverEntry[]): void {
|
|
155
|
+
for (const entry of entries) {
|
|
156
|
+
const itemIndex = id2TocItemIndex[entry.target.id]!;
|
|
157
|
+
const tocItem = runtimeToc.value?.[itemIndex];
|
|
158
|
+
|
|
159
|
+
if (tocItem) {
|
|
160
|
+
tocItem._active = entry.isIntersecting
|
|
161
|
+
? 2
|
|
162
|
+
: tocItem._active === 1
|
|
163
|
+
? 1
|
|
164
|
+
: 0;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
onMounted(() => {
|
|
170
|
+
watch(
|
|
171
|
+
[topicData, topicLocation],
|
|
172
|
+
() => {
|
|
173
|
+
disableLiveToc();
|
|
174
|
+
|
|
175
|
+
if (!topicData.value.location || !topicLocation.value) return;
|
|
176
|
+
|
|
177
|
+
enableLiveToc();
|
|
178
|
+
},
|
|
179
|
+
{ immediate: true },
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
onUnmounted(() => {
|
|
184
|
+
disableLiveToc();
|
|
185
|
+
});
|
|
186
|
+
</script>
|
|
187
|
+
|
|
188
|
+
<template>
|
|
189
|
+
<section :class="$style.topicToc">
|
|
190
|
+
<TreeContainer v-if="runtimeToc?.length > 0" :key="tocStateKey">
|
|
191
|
+
<TopicTocItem
|
|
192
|
+
v-for="tocItem of runtimeToc"
|
|
193
|
+
v-memo="[tocStateKey, tocItem.id, tocItem._active]"
|
|
194
|
+
:active="!!tocItem._active"
|
|
195
|
+
:tocItem
|
|
196
|
+
/>
|
|
197
|
+
</TreeContainer>
|
|
198
|
+
<div v-else :class="$style.tocEmpty">{{ phrase.empty_toc }}</div>
|
|
199
|
+
</section>
|
|
200
|
+
</template>
|
|
201
|
+
|
|
202
|
+
<style lang="scss" module>
|
|
203
|
+
.topicToc {
|
|
204
|
+
flex: 1;
|
|
205
|
+
overflow: auto;
|
|
206
|
+
@include scroll();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.tocEmpty {
|
|
210
|
+
padding: var(--gap);
|
|
211
|
+
text-align: center;
|
|
212
|
+
color: var(--textMuted);
|
|
213
|
+
}
|
|
214
|
+
</style>
|
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
<script lang="ts" setup>
|
|
2
|
-
import type { TocItem } from '@erudit/shared/bitran/toc';
|
|
3
|
-
|
|
4
|
-
const props = defineProps<{ tocItem: TocItem; active: boolean }>();
|
|
5
|
-
const productName = props.tocItem.productName;
|
|
6
|
-
const productIcon = await useBitranElementIcon(productName);
|
|
7
|
-
const productPhrase = await useBitranElementLanguage(productName);
|
|
8
|
-
|
|
9
|
-
const pretty = useFormatText();
|
|
10
|
-
|
|
11
|
-
function tocItemClick(event: MouseEvent): void {
|
|
12
|
-
navigateTo({
|
|
13
|
-
query: {
|
|
14
|
-
element: props.tocItem.id,
|
|
15
|
-
},
|
|
16
|
-
//hash: props.tocItem.id, // Apply page hash as requested in the comment
|
|
17
|
-
});
|
|
18
|
-
event.preventDefault();
|
|
19
|
-
event.stopPropagation();
|
|
20
|
-
}
|
|
21
|
-
</script>
|
|
22
|
-
|
|
23
|
-
<template>
|
|
24
|
-
<TreeItem
|
|
25
|
-
:label="pretty(tocItem.title || productPhrase('_element_title'))"
|
|
26
|
-
:level="tocItem.level"
|
|
27
|
-
:link="`?element=${tocItem.id}`"
|
|
28
|
-
:svg="productIcon"
|
|
29
|
-
:active
|
|
30
|
-
@click="tocItemClick"
|
|
31
|
-
/>
|
|
32
|
-
</template>
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import type { TocItem } from '@erudit/shared/bitran/toc';
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{ tocItem: TocItem; active: boolean }>();
|
|
5
|
+
const productName = props.tocItem.productName;
|
|
6
|
+
const productIcon = await useBitranElementIcon(productName);
|
|
7
|
+
const productPhrase = await useBitranElementLanguage(productName);
|
|
8
|
+
|
|
9
|
+
const pretty = useFormatText();
|
|
10
|
+
|
|
11
|
+
function tocItemClick(event: MouseEvent): void {
|
|
12
|
+
navigateTo({
|
|
13
|
+
query: {
|
|
14
|
+
element: props.tocItem.id,
|
|
15
|
+
},
|
|
16
|
+
//hash: props.tocItem.id, // Apply page hash as requested in the comment
|
|
17
|
+
});
|
|
18
|
+
event.preventDefault();
|
|
19
|
+
event.stopPropagation();
|
|
20
|
+
}
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<TreeItem
|
|
25
|
+
:label="pretty(tocItem.title || productPhrase('_element_title'))"
|
|
26
|
+
:level="tocItem.level"
|
|
27
|
+
:link="`?element=${tocItem.id}`"
|
|
28
|
+
:svg="productIcon"
|
|
29
|
+
:active
|
|
30
|
+
@click="tocItemClick"
|
|
31
|
+
/>
|
|
32
|
+
</template>
|
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
<script lang="ts" setup>
|
|
2
|
-
const props = defineProps<{ stick?: 'top' | 'bottom' }>();
|
|
3
|
-
const style = useCssModule();
|
|
4
|
-
|
|
5
|
-
const classes = computed(() => {
|
|
6
|
-
const _classes: string[] = [style.asideOverlayPane];
|
|
7
|
-
|
|
8
|
-
if (props.stick === 'bottom') _classes.push(style.stickBottom);
|
|
9
|
-
|
|
10
|
-
return _classes;
|
|
11
|
-
});
|
|
12
|
-
</script>
|
|
13
|
-
|
|
14
|
-
<template>
|
|
15
|
-
<section :class="classes">
|
|
16
|
-
<slot></slot>
|
|
17
|
-
</section>
|
|
18
|
-
</template>
|
|
19
|
-
|
|
20
|
-
<style lang="scss" module>
|
|
21
|
-
@use '$/def/bp';
|
|
22
|
-
|
|
23
|
-
.asideOverlayPane {
|
|
24
|
-
position: absolute;
|
|
25
|
-
z-index: 10;
|
|
26
|
-
top: 0;
|
|
27
|
-
left: 0;
|
|
28
|
-
width: 100%;
|
|
29
|
-
height: 100%;
|
|
30
|
-
background: var(--bgAside);
|
|
31
|
-
|
|
32
|
-
display: flex;
|
|
33
|
-
flex-direction: column;
|
|
34
|
-
justify-content: start;
|
|
35
|
-
|
|
36
|
-
&.stickBottom {
|
|
37
|
-
justify-content: end;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
</style>
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
const props = defineProps<{ stick?: 'top' | 'bottom' }>();
|
|
3
|
+
const style = useCssModule();
|
|
4
|
+
|
|
5
|
+
const classes = computed(() => {
|
|
6
|
+
const _classes: string[] = [style.asideOverlayPane];
|
|
7
|
+
|
|
8
|
+
if (props.stick === 'bottom') _classes.push(style.stickBottom);
|
|
9
|
+
|
|
10
|
+
return _classes;
|
|
11
|
+
});
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<section :class="classes">
|
|
16
|
+
<slot></slot>
|
|
17
|
+
</section>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<style lang="scss" module>
|
|
21
|
+
@use '$/def/bp';
|
|
22
|
+
|
|
23
|
+
.asideOverlayPane {
|
|
24
|
+
position: absolute;
|
|
25
|
+
z-index: 10;
|
|
26
|
+
top: 0;
|
|
27
|
+
left: 0;
|
|
28
|
+
width: 100%;
|
|
29
|
+
height: 100%;
|
|
30
|
+
background: var(--bgAside);
|
|
31
|
+
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
justify-content: start;
|
|
35
|
+
|
|
36
|
+
&.stickBottom {
|
|
37
|
+
justify-content: end;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
</style>
|