@difizen/libro-toc 0.0.2-alpha.0
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/LICENSE +21 -0
- package/README.md +1 -0
- package/es/cell-toc-provider.d.ts +9 -0
- package/es/cell-toc-provider.d.ts.map +1 -0
- package/es/cell-toc-provider.js +46 -0
- package/es/index.d.ts +6 -0
- package/es/index.d.ts.map +1 -0
- package/es/index.js +5 -0
- package/es/index.less +66 -0
- package/es/libro-toc-color-registry.d.ts +6 -0
- package/es/libro-toc-color-registry.d.ts.map +1 -0
- package/es/libro-toc-color-registry.js +46 -0
- package/es/module.d.ts +14 -0
- package/es/module.d.ts.map +1 -0
- package/es/module.js +40 -0
- package/es/provider/html.d.ts +39 -0
- package/es/provider/html.d.ts.map +1 -0
- package/es/provider/html.js +54 -0
- package/es/provider/markdown-toc-provider.d.ts +11 -0
- package/es/provider/markdown-toc-provider.d.ts.map +1 -0
- package/es/provider/markdown-toc-provider.js +43 -0
- package/es/provider/markdown.d.ts +23 -0
- package/es/provider/markdown.d.ts.map +1 -0
- package/es/provider/markdown.js +142 -0
- package/es/provider/output-toc-provider.d.ts +10 -0
- package/es/provider/output-toc-provider.d.ts.map +1 -0
- package/es/provider/output-toc-provider.js +67 -0
- package/es/toc-collapse-service.d.ts +10 -0
- package/es/toc-collapse-service.d.ts.map +1 -0
- package/es/toc-collapse-service.js +141 -0
- package/es/toc-configuration.d.ts +7 -0
- package/es/toc-configuration.d.ts.map +1 -0
- package/es/toc-configuration.js +34 -0
- package/es/toc-contribution.d.ts +11 -0
- package/es/toc-contribution.d.ts.map +1 -0
- package/es/toc-contribution.js +63 -0
- package/es/toc-manager.d.ts +10 -0
- package/es/toc-manager.d.ts.map +1 -0
- package/es/toc-manager.js +32 -0
- package/es/toc-protocol.d.ts +107 -0
- package/es/toc-protocol.d.ts.map +1 -0
- package/es/toc-protocol.js +35 -0
- package/es/toc-provider.d.ts +37 -0
- package/es/toc-provider.d.ts.map +1 -0
- package/es/toc-provider.js +181 -0
- package/es/toc-view.d.ts +33 -0
- package/es/toc-view.d.ts.map +1 -0
- package/es/toc-view.js +245 -0
- package/package.json +62 -0
- package/src/cell-toc-provider.ts +31 -0
- package/src/index.less +66 -0
- package/src/index.ts +5 -0
- package/src/libro-toc-color-registry.ts +27 -0
- package/src/module.ts +58 -0
- package/src/provider/html.ts +61 -0
- package/src/provider/markdown-toc-provider.ts +34 -0
- package/src/provider/markdown.ts +182 -0
- package/src/provider/output-toc-provider.ts +53 -0
- package/src/toc-collapse-service.ts +96 -0
- package/src/toc-configuration.ts +22 -0
- package/src/toc-contribution.ts +34 -0
- package/src/toc-manager.ts +27 -0
- package/src/toc-protocol.ts +130 -0
- package/src/toc-provider.ts +154 -0
- package/src/toc-view.tsx +225 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { LibroView } from '@difizen/libro-core';
|
|
2
|
+
import type { Disposable, Event } from '@difizen/mana-app';
|
|
3
|
+
import { inject, notEmpty, prop, transient, watch } from '@difizen/mana-app';
|
|
4
|
+
import { Emitter, DisposableCollection } from '@difizen/mana-app';
|
|
5
|
+
|
|
6
|
+
import { LibroCellTOCProvider } from './cell-toc-provider.js';
|
|
7
|
+
import type { CellTOCProvider, IHeading } from './toc-protocol.js';
|
|
8
|
+
import { TOCProviderOption } from './toc-protocol.js';
|
|
9
|
+
|
|
10
|
+
export type LibroTOCProviderFactory = (option: TOCProviderOption) => LibroTOCProvider;
|
|
11
|
+
export const LibroTOCProviderFactory = Symbol('LibroTOCProviderFactory');
|
|
12
|
+
|
|
13
|
+
@transient()
|
|
14
|
+
export class LibroTOCProvider implements Disposable {
|
|
15
|
+
protected libroCellTOCProvider: LibroCellTOCProvider;
|
|
16
|
+
|
|
17
|
+
@prop() protected providerMap = new Map<string, CellTOCProvider>();
|
|
18
|
+
|
|
19
|
+
@prop() headings: IHeading[] = [];
|
|
20
|
+
|
|
21
|
+
@prop()
|
|
22
|
+
protected view: LibroView;
|
|
23
|
+
|
|
24
|
+
protected toDispose = new DisposableCollection();
|
|
25
|
+
protected toDisposeWatcher = new DisposableCollection();
|
|
26
|
+
|
|
27
|
+
disposed = false;
|
|
28
|
+
|
|
29
|
+
protected activeCellChangeEmitter = new Emitter<IHeading>();
|
|
30
|
+
|
|
31
|
+
get activeCellChange(): Event<IHeading> {
|
|
32
|
+
return this.activeCellChangeEmitter.event;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
@inject(TOCProviderOption) option: TOCProviderOption,
|
|
37
|
+
@inject(LibroCellTOCProvider) libroCellTOCProvider: LibroCellTOCProvider,
|
|
38
|
+
) {
|
|
39
|
+
this.view = option.view as LibroView;
|
|
40
|
+
this.libroCellTOCProvider = libroCellTOCProvider;
|
|
41
|
+
|
|
42
|
+
this.initUpdateWatch();
|
|
43
|
+
this.updateTOC();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
protected initUpdateWatch() {
|
|
47
|
+
this.toDispose.push(
|
|
48
|
+
watch(this.view.model, 'activeIndex', this.handleActiveCellChange),
|
|
49
|
+
);
|
|
50
|
+
this.toDispose.push(watch(this.view.model, 'cells', this.onCellsChanged));
|
|
51
|
+
this.setupUpdaterWatcher();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
protected setupUpdaterWatcher() {
|
|
55
|
+
this.toDisposeWatcher.dispose();
|
|
56
|
+
this.toDisposeWatcher = new DisposableCollection();
|
|
57
|
+
this.getCellTocProviders().map((item) => {
|
|
58
|
+
this.toDisposeWatcher.push(item.updateWatcher(this.updateTOC));
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
protected handleActiveCellChange = () => {
|
|
63
|
+
const header = this.getHeadingByCellIndex(this.view.model.activeIndex);
|
|
64
|
+
if (header) {
|
|
65
|
+
this.activeCellChangeEmitter.fire(header);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
protected updateTOC = () => {
|
|
70
|
+
this.headings = this.getHeadings();
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
protected onActiveCellChanged = () => {
|
|
74
|
+
this.updateTOC();
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
protected onCellsChanged = () => {
|
|
78
|
+
this.setupUpdaterWatcher();
|
|
79
|
+
this.updateTOC();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
protected onContentChanged = () => {
|
|
83
|
+
this.updateTOC();
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
getCellTocProviderList() {
|
|
87
|
+
if (!this.view) {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
const cells = this.view.model.cells;
|
|
91
|
+
return cells.map((cell) => {
|
|
92
|
+
let tocProvider: CellTOCProvider | undefined;
|
|
93
|
+
if (this.providerMap.has(cell.id)) {
|
|
94
|
+
tocProvider = this.providerMap.get(cell.id);
|
|
95
|
+
} else {
|
|
96
|
+
tocProvider = this.libroCellTOCProvider.createCellTOCProvider(cell);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (tocProvider) {
|
|
100
|
+
this.providerMap.set(cell.id, tocProvider);
|
|
101
|
+
}
|
|
102
|
+
return { cellId: cell.model.id, tocProvider };
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
protected getCellTocProviders() {
|
|
107
|
+
return this.getCellTocProviderList()
|
|
108
|
+
.map((item) => item.tocProvider)
|
|
109
|
+
.filter(notEmpty);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
protected getHeadings() {
|
|
113
|
+
return this.getCellTocProviderList()
|
|
114
|
+
.map((item) => {
|
|
115
|
+
if (item.tocProvider !== undefined) {
|
|
116
|
+
const headings = item.tocProvider.getHeadings();
|
|
117
|
+
headings.forEach(
|
|
118
|
+
(heading) =>
|
|
119
|
+
(heading.dataset = { ...heading.dataset, cellId: item.cellId }),
|
|
120
|
+
);
|
|
121
|
+
return headings;
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
124
|
+
})
|
|
125
|
+
.filter(notEmpty)
|
|
126
|
+
.flat();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
selectCellByHeading(heading: IHeading) {
|
|
130
|
+
const cellId = heading?.dataset?.['cellId'];
|
|
131
|
+
if (!cellId) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const cell = this.view.model.cells.find((item) => item.model.id === cellId);
|
|
135
|
+
if (cell) {
|
|
136
|
+
this.view.selectCell(cell);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
getHeadingByCellIndex(index: number) {
|
|
141
|
+
const cell = this.view.model.cells[index];
|
|
142
|
+
return this.headings.find((item) => item?.dataset?.['cellId'] === cell.model.id);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
dispose() {
|
|
146
|
+
if (this.disposed) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
this.toDispose.dispose();
|
|
150
|
+
this.toDisposeWatcher.dispose();
|
|
151
|
+
this.providerMap.clear();
|
|
152
|
+
this.disposed = true;
|
|
153
|
+
}
|
|
154
|
+
}
|
package/src/toc-view.tsx
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { ArrowDown, ArrowRight } from '@difizen/libro-core';
|
|
2
|
+
import type { DisplayView, LibroView } from '@difizen/libro-core';
|
|
3
|
+
import { ConfigurationService } from '@difizen/mana-app';
|
|
4
|
+
import { getOrigin, prop, useInject } from '@difizen/mana-app';
|
|
5
|
+
import { BaseView, view, ViewInstance } from '@difizen/mana-app';
|
|
6
|
+
import { inject, transient } from '@difizen/mana-app';
|
|
7
|
+
import { l10n } from '@difizen/mana-l10n';
|
|
8
|
+
import React, { useRef } from 'react';
|
|
9
|
+
|
|
10
|
+
import { TOCVisible } from './toc-configuration.js';
|
|
11
|
+
import { LibroTOCManager } from './toc-manager.js';
|
|
12
|
+
import type { IHeading } from './toc-protocol.js';
|
|
13
|
+
import type { LibroTOCProvider } from './toc-provider.js';
|
|
14
|
+
import './index.less';
|
|
15
|
+
|
|
16
|
+
interface DisplayHeading extends IHeading {
|
|
17
|
+
hasChild: boolean;
|
|
18
|
+
visible: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface TocItemProps {
|
|
22
|
+
heading: DisplayHeading;
|
|
23
|
+
active: boolean;
|
|
24
|
+
onClick: (heading: DisplayHeading) => void;
|
|
25
|
+
onToggle: () => void;
|
|
26
|
+
headingCollapsed: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const TocItem: React.FC<TocItemProps> = ({
|
|
30
|
+
heading,
|
|
31
|
+
onClick,
|
|
32
|
+
active,
|
|
33
|
+
headingCollapsed,
|
|
34
|
+
onToggle,
|
|
35
|
+
}) => {
|
|
36
|
+
const instance = useInject<TOCView>(ViewInstance);
|
|
37
|
+
const { id, text, level, hasChild, visible } = heading;
|
|
38
|
+
if (!visible) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const handleClick = () => {
|
|
42
|
+
if (!id) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const element = document.getElementById(id);
|
|
47
|
+
if (element) {
|
|
48
|
+
element.scrollIntoView();
|
|
49
|
+
}
|
|
50
|
+
onClick(heading);
|
|
51
|
+
};
|
|
52
|
+
return (
|
|
53
|
+
<>
|
|
54
|
+
<div
|
|
55
|
+
className={`markdown-toc-container-anchor ${active ? 'active' : ''}`}
|
|
56
|
+
style={{
|
|
57
|
+
paddingLeft: instance.getHeadingIndentSize(level) + (hasChild ? 0 : 20),
|
|
58
|
+
}}
|
|
59
|
+
onClick={handleClick}
|
|
60
|
+
>
|
|
61
|
+
{hasChild && (
|
|
62
|
+
<span
|
|
63
|
+
onClick={(e) => {
|
|
64
|
+
e.stopPropagation();
|
|
65
|
+
onToggle();
|
|
66
|
+
}}
|
|
67
|
+
className="libro-toc-collapsed"
|
|
68
|
+
>
|
|
69
|
+
{headingCollapsed ? <ArrowRight /> : <ArrowDown />}
|
|
70
|
+
</span>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
{text}
|
|
74
|
+
</div>
|
|
75
|
+
<div className={`markdown-toc-container-anchor-shot ${active ? 'active' : ''}`} />
|
|
76
|
+
</>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const TocRender = () => {
|
|
81
|
+
const instance = useInject<TOCView>(ViewInstance);
|
|
82
|
+
const containRef = useRef<HTMLDivElement>(null);
|
|
83
|
+
|
|
84
|
+
const handleClick = (heading: IHeading) => {
|
|
85
|
+
instance.activeHeading = heading;
|
|
86
|
+
instance.tocProvider?.selectCellByHeading(heading);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div className="markdown-toc-container" ref={containRef}>
|
|
91
|
+
<div className="markdown-toc-container-title">{instance.tocTitle}</div>
|
|
92
|
+
{instance.getDisplayHeadings().map((heading) => {
|
|
93
|
+
const collapsed = instance.isHeadingCollapsed(heading);
|
|
94
|
+
return (
|
|
95
|
+
<TocItem
|
|
96
|
+
active={instance.activeHeading?.id === heading.id}
|
|
97
|
+
heading={heading}
|
|
98
|
+
key={heading.id}
|
|
99
|
+
onClick={handleClick}
|
|
100
|
+
headingCollapsed={collapsed}
|
|
101
|
+
onToggle={() => {
|
|
102
|
+
if (heading.id) {
|
|
103
|
+
instance.headingCollapseState.set(heading.id, !collapsed);
|
|
104
|
+
}
|
|
105
|
+
}}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
})}
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
@transient()
|
|
114
|
+
@view('libro-toc-view')
|
|
115
|
+
export class TOCView extends BaseView implements DisplayView {
|
|
116
|
+
parent: LibroView | undefined = undefined;
|
|
117
|
+
protected configurationService: ConfigurationService;
|
|
118
|
+
|
|
119
|
+
override view = TocRender;
|
|
120
|
+
|
|
121
|
+
@prop()
|
|
122
|
+
isDisplay: boolean;
|
|
123
|
+
|
|
124
|
+
@prop()
|
|
125
|
+
tocProvider?: LibroTOCProvider;
|
|
126
|
+
|
|
127
|
+
@prop()
|
|
128
|
+
activeHeading: IHeading | undefined;
|
|
129
|
+
|
|
130
|
+
protected libroTOCManager: LibroTOCManager;
|
|
131
|
+
|
|
132
|
+
@prop()
|
|
133
|
+
tocTitle: string = l10n.t('大纲');
|
|
134
|
+
|
|
135
|
+
@prop()
|
|
136
|
+
headingCollapseState = new Map<string, boolean>();
|
|
137
|
+
|
|
138
|
+
constructor(
|
|
139
|
+
@inject(LibroTOCManager) libroTOCManager: LibroTOCManager,
|
|
140
|
+
@inject(ConfigurationService) configurationService: ConfigurationService,
|
|
141
|
+
) {
|
|
142
|
+
super();
|
|
143
|
+
this.libroTOCManager = libroTOCManager;
|
|
144
|
+
this.configurationService = configurationService;
|
|
145
|
+
this.configurationService
|
|
146
|
+
.get(TOCVisible)
|
|
147
|
+
.then((value) => {
|
|
148
|
+
this.isDisplay = value;
|
|
149
|
+
return;
|
|
150
|
+
})
|
|
151
|
+
.catch(() => {
|
|
152
|
+
//
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
getHeadingIndentSize(level: number): number {
|
|
157
|
+
return level * 12;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
override onViewMount() {
|
|
161
|
+
if (!this.tocProvider && this.parent) {
|
|
162
|
+
getOrigin(this.parent.initialized)
|
|
163
|
+
.then(() => {
|
|
164
|
+
this.tocProvider = this.libroTOCManager.getTOCProvider(this.parent!);
|
|
165
|
+
this.tocProvider.activeCellChange((header) => {
|
|
166
|
+
this.activeHeading = header;
|
|
167
|
+
});
|
|
168
|
+
return;
|
|
169
|
+
})
|
|
170
|
+
.catch(() => {
|
|
171
|
+
//
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
getDisplayHeadings(): DisplayHeading[] {
|
|
177
|
+
const headings = this.tocProvider?.headings ?? [];
|
|
178
|
+
|
|
179
|
+
return headings.map((item, index) => {
|
|
180
|
+
return {
|
|
181
|
+
...item,
|
|
182
|
+
visible: this.isHeadingVisible(item, index, headings),
|
|
183
|
+
hasChild: this.hasChildren(item, index, headings),
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
protected isHeadingVisible(heading: IHeading, index: number, list: IHeading[]) {
|
|
189
|
+
if (index === 0) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
let headingCollapsed = false;
|
|
193
|
+
let parent = heading;
|
|
194
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
195
|
+
const current = list[i];
|
|
196
|
+
if (current.level < parent.level) {
|
|
197
|
+
parent = current;
|
|
198
|
+
if (this.isHeadingCollapsed(parent)) {
|
|
199
|
+
headingCollapsed = true;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return !headingCollapsed;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
isHeadingCollapsed(heading: IHeading) {
|
|
209
|
+
return heading.id ? this.headingCollapseState.get(heading.id) ?? false : false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
protected hasChildren(current: IHeading, index: number, list: IHeading[]) {
|
|
213
|
+
if (index === list.length - 1) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
if (current.level < list[index + 1].level) {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
override dispose() {
|
|
223
|
+
this.tocProvider?.dispose();
|
|
224
|
+
}
|
|
225
|
+
}
|