UNPKG

@jupyterlab/toc

Version:

JupyterLab - Table of Contents widget

229 lines 7.26 kB
// 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 { Signal } from '@lumino/signaling'; import { TableOfContents } from './tokens'; /** * Abstract table of contents model. */ export class TableOfContentsModel extends VDomModel { /** * Constructor * * @param widget The widget to search in * @param configuration Default model configuration */ constructor(widget, configuration) { super(); this.widget = widget; this._activeHeading = null; this._activeHeadingChanged = new Signal(this); this._collapseChanged = new Signal(this); this._configuration = configuration !== null && configuration !== void 0 ? configuration : { ...TableOfContents.defaultConfig }; this._headings = new Array(); this._headingsChanged = new Signal(this); this._isActive = false; this._isRefreshing = false; this._needsRefreshing = false; } /** * Current active entry. * * @returns table of contents active entry */ get activeHeading() { return this._activeHeading; } /** * Signal emitted when the active heading changes. */ get activeHeadingChanged() { return this._activeHeadingChanged; } /** * Signal emitted when a table of content section collapse state changes. */ get collapseChanged() { return this._collapseChanged; } /** * Model configuration */ get configuration() { return this._configuration; } /** * List of headings. * * @returns table of contents list of headings */ get headings() { return this._headings; } /** * Signal emitted when the headings changes. */ get headingsChanged() { 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() { return this._isActive; } set isActive(v) { 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. */ get isAlwaysActive() { return false; } /** * List of configuration options supported by the model. */ get supportedOptions() { return ['maximalDepth']; } /** * Document title */ get title() { return this._title; } set title(v) { if (v !== this._title) { this._title = v; this.stateChanged.emit(); } } /** * Refresh the headings list. */ async refresh() { 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 && !this._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, emitSignal = true) { if (this._activeHeading !== heading) { this._activeHeading = heading; this.stateChanged.emit(); } if (emitSignal) { // Always emit the signal to trigger a scroll even if the value did not change this._activeHeadingChanged.emit(this._activeHeading); } } /** * Model configuration setter. * * @param c New configuration */ setConfiguration(c) { const newConfiguration = { ...this._configuration, ...c }; if (!JSONExt.deepEqual(this._configuration, newConfiguration)) { this._configuration = newConfiguration; 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) { var _a, _b; if (options.heading) { options.heading.collapsed = (_a = options.collapsed) !== null && _a !== void 0 ? _a : !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 = (_b = options.collapsed) !== null && _b !== void 0 ? _b : !this.headings.some(h => { var _a; return !((_a = h.collapsed) !== null && _a !== void 0 ? _a : false); }); this.headings.forEach(h => (h.collapsed = newState)); this.stateChanged.emit(); this._collapseChanged.emit(null); } } /** * Test if two headings are equal or not. * * @param heading1 First heading * @param heading2 Second heading * @returns Whether the headings are equal. */ isHeadingEqual(heading1, heading2) { return (heading1.level === heading2.level && heading1.text === heading2.text && heading1.prefix === heading2.prefix); } /** * 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 equal. */ _areHeadingsEqual(headings1, headings2) { if (headings1.length === headings2.length) { for (let i = 0; i < headings1.length; i++) { if (!this.isHeadingEqual(headings1[i], headings2[i])) { return false; } } return true; } return false; } } //# sourceMappingURL=model.js.map