-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathmodel.ts
285 lines (261 loc) · 7.69 KB
/
model.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import { VDomModel } from '@jupyterlab/ui-components';
import { JSONExt } from '@lumino/coreutils';
import { ISignal, Signal } from '@lumino/signaling';
import { Widget } from '@lumino/widgets';
import { TableOfContents } from './tokens';
/**
* Abstract table of contents model.
*/
export abstract class TableOfContentsModel<
H extends TableOfContents.IHeading,
T extends Widget = Widget
>
extends VDomModel
implements TableOfContents.IModel<H>
{
/**
* Constructor
*
* @param widget The widget to search in
* @param configuration Default model configuration
*/
constructor(protected widget: T, configuration?: TableOfContents.IConfig) {
super();
this._activeHeading = null;
this._activeHeadingChanged = new Signal<
TableOfContentsModel<H, T>,
H | null
>(this);
this._collapseChanged = new Signal<TableOfContentsModel<H, T>, H>(this);
this._configuration = configuration ?? { ...TableOfContents.defaultConfig };
this._headings = new Array<H>();
this._headingsChanged = new Signal<TableOfContentsModel<H, T>, void>(this);
this._isActive = false;
this._isRefreshing = false;
this._needsRefreshing = false;
}
/**
* Current active entry.
*
* @returns table of contents active entry
*/
get activeHeading(): H | null {
return this._activeHeading;
}
/**
* Signal emitted when the active heading changes.
*/
get activeHeadingChanged(): ISignal<TableOfContents.IModel<H>, H | null> {
return this._activeHeadingChanged;
}
/**
* Signal emitted when a table of content section collapse state changes.
*/
get collapseChanged(): ISignal<TableOfContents.IModel<H>, H | null> {
return this._collapseChanged;
}
/**
* Model configuration
*/
get configuration(): TableOfContents.IConfig {
return this._configuration;
}
/**
* Type of document supported by the model.
*
* #### Notes
* A `data-document-type` attribute with this value will be set
* on the tree view `.jp-TableOfContents-content[data-document-type="..."]`
*/
abstract readonly documentType: string;
/**
* List of headings.
*
* @returns table of contents list of headings
*/
get headings(): H[] {
return this._headings;
}
/**
* Signal emitted when the headings changes.
*/
get headingsChanged(): ISignal<TableOfContents.IModel<H>, void> {
return this._headingsChanged;
}
/**
* Whether the model is active or not.
*
* #### Notes
* An active model means it is displayed in the table of contents.
* This can be used by subclass to limit updating the headings.
*/
get isActive(): boolean {
return this._isActive;
}
set isActive(v: boolean) {
this._isActive = v;
// Refresh on activation expect if it is always active
// => a ToC model is always active e.g. when displaying numbering in the document
if (this._isActive && !this.isAlwaysActive) {
this.refresh().catch(reason => {
console.error('Failed to refresh ToC model.', reason);
});
}
}
/**
* Whether the model gets updated even if the table of contents panel
* is hidden or not.
*
* #### Notes
* For example, ToC models use to add title numbering will
* set this to true.
*/
protected get isAlwaysActive(): boolean {
return false;
}
/**
* List of configuration options supported by the model.
*/
get supportedOptions(): (keyof TableOfContents.IConfig)[] {
return ['maximalDepth'];
}
/**
* Document title
*/
get title(): string | undefined {
return this._title;
}
set title(v: string | undefined) {
if (v !== this._title) {
this._title = v;
this.stateChanged.emit();
}
}
/**
* Abstract function that will produce the headings for a document.
*
* @returns The list of new headings or `null` if nothing needs to be updated.
*/
protected abstract getHeadings(): Promise<H[] | null>;
/**
* Refresh the headings list.
*/
async refresh(): Promise<void> {
if (this._isRefreshing) {
// Schedule a refresh if one is in progress
this._needsRefreshing = true;
return Promise.resolve();
}
this._isRefreshing = true;
try {
const newHeadings = await this.getHeadings();
if (this._needsRefreshing) {
this._needsRefreshing = false;
this._isRefreshing = false;
return this.refresh();
}
if (
newHeadings &&
!Private.areHeadingsEqual(newHeadings, this._headings)
) {
this._headings = newHeadings;
this.stateChanged.emit();
this._headingsChanged.emit();
}
} finally {
this._isRefreshing = false;
}
}
/**
* Set a new active heading.
*
* @param heading The new active heading
* @param emitSignal Whether to emit the activeHeadingChanged signal or not.
*/
setActiveHeading(heading: H | null, emitSignal = true): void {
if (this._activeHeading !== heading) {
this._activeHeading = heading;
this.stateChanged.emit();
if (emitSignal) {
this._activeHeadingChanged.emit(heading);
}
}
}
/**
* Model configuration setter.
*
* @param c New configuration
*/
setConfiguration(c: Partial<TableOfContents.IConfig>): void {
const newConfiguration = { ...this._configuration, ...c };
if (!JSONExt.deepEqual(this._configuration, newConfiguration)) {
this._configuration = newConfiguration as TableOfContents.IConfig;
this.refresh().catch(reason => {
console.error('Failed to update the table of contents.', reason);
});
}
}
/**
* Callback on heading collapse.
*
* @param options.heading The heading to change state (all headings if not provided)
* @param options.collapsed The new collapsed status (toggle existing status if not provided)
*/
toggleCollapse(options: { heading?: H; collapsed?: boolean }): void {
if (options.heading) {
options.heading.collapsed =
options.collapsed ?? !options.heading.collapsed;
this.stateChanged.emit();
this._collapseChanged.emit(options.heading);
} else {
// Use the provided state or collapsed all except if all are collapsed
const newState =
options.collapsed ?? !this.headings.some(h => !(h.collapsed ?? false));
this.headings.forEach(h => (h.collapsed = newState));
this.stateChanged.emit();
this._collapseChanged.emit(null);
}
}
private _activeHeading: H | null;
private _activeHeadingChanged: Signal<TableOfContentsModel<H, T>, H | null>;
private _collapseChanged: Signal<TableOfContentsModel<H, T>, H | null>;
private _configuration: TableOfContents.IConfig;
private _headings: H[];
private _headingsChanged: Signal<TableOfContentsModel<H, T>, void>;
private _isActive: boolean;
private _isRefreshing: boolean;
private _needsRefreshing: boolean;
private _title?: string;
}
/**
* Private functions namespace
*/
namespace Private {
/**
* Test if two list of headings are equal or not.
*
* @param headings1 First list of headings
* @param headings2 Second list of headings
* @returns Whether the array are identical or not.
*/
export function areHeadingsEqual(
headings1: TableOfContents.IHeading[],
headings2: TableOfContents.IHeading[]
): boolean {
if (headings1.length === headings2.length) {
for (let i = 0; i < headings1.length; i++) {
if (
headings1[i].level !== headings2[i].level ||
headings1[i].text !== headings2[i].text ||
headings1[i].prefix !== headings2[i].prefix
) {
return false;
}
}
return true;
}
return false;
}
}