Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions packages/survey-core/src/survey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1664,6 +1664,22 @@ export class SurveyModel extends SurveyElementCore
* @see showTOC
*/
@property() tocLocation: "left" | "right";
/**
* Specifies the depth of nested panels included in the table of contents.
*
* Default value: `0` (nested panels are not included)
*
* Set this property to a positive number to include panels from pages:
*
* - `1` - includes direct child panels of a page
* - `2` - includes child and grandchild panels
* - `3` and greater - includes deeper nested levels accordingly
*
* Only panels with a visible title are shown in the table of contents.
* @see showTOC
* @see tocLocation
*/
@property({ defaultValue: 0 }) panelsInTocLevel: number;
/**
* Specifies whether to display the [survey title](https://surveyjs.io/form-library/documentation/api-reference/survey-data-model#title).
*
Expand Down Expand Up @@ -8651,6 +8667,13 @@ Serializer.addClass("survey", [
dependsOn: ["showTOC"],
visibleIf: (survey: any) => { return !!survey && survey.showTOC; }
},
{
name: "panelsInTocLevel:number",
default: 0,
minValue: 0,
dependsOn: ["showTOC"],
visibleIf: (survey: any) => { return !!survey && survey.showTOC; }
},
{ name: "readOnly:boolean" },
{ name: "mode", visible: false, isSerializable: false },
{ name: "storeOthersAsComment:boolean", default: true },
Expand Down
43 changes: 40 additions & 3 deletions packages/survey-core/src/surveyToc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,50 @@ export function createTOCListModel(survey: SurveyModel, onAction?: () => void):
listModel.setItems(getTOCItems(survey, onAction));
};
survey.registerFunctionOnPropertyValueChanged("pages", updatePagesFunc, "toc");
survey.registerFunctionOnPropertyValueChanged("panelsInTocLevel", updatePagesFunc, "toc");
survey.onEndLoadingFromJson.add(updatePagesFunc);
return listModel;
}

interface ITOCItemInfo {
element: PanelModelBase;
level: number;
}

function fillPanelTOCItems(container: PanelModelBase, level: number, maxLevel: number, target: Array<ITOCItemInfo>): void {
if (!container || level > maxLevel || !(<any>container).elements) return;
const elements = (<any>container).elements;
for (let i = 0; i < elements.length; i++) {
const el = elements[i];
if (!!el && el.isPanel) {
const panel = <PanelModelBase><any>el;
target.push({ element: panel, level: level });
fillPanelTOCItems(panel, level + 1, maxLevel, target);
}
}
}

function getTOCItemsSource(survey: SurveyModel): Array<ITOCItemInfo> {
const pagesSource: PanelModelBase[] = survey.pages || [];
const itemsSource: Array<ITOCItemInfo> = pagesSource.map(page => ({ element: page, level: 0 }));
const maxPanelLevel = Math.max(0, survey.panelsInTocLevel || 0);
if (maxPanelLevel > 0) {
for (let i = 0; i < pagesSource.length; i++) {
fillPanelTOCItems(pagesSource[i], 1, maxPanelLevel, itemsSource);
}
}
return itemsSource;
}

function getTOCItems(survey: SurveyModel, onAction: () => void) {
const pagesSource: PanelModelBase[] = survey.pages;
var items = (pagesSource || []).map(page => {
const itemsSource = getTOCItemsSource(survey);
var items = itemsSource.map(itemInfo => {
const page = itemInfo.element;
const level = itemInfo.level;
return new Action({
id: page.name,
locTitle: page.locNavigationTitle,
data: { tocLevel: level },
action: () => {
DomDocumentHelper.activeElementBlur();
!!onAction && onAction();
Expand All @@ -76,7 +110,9 @@ function getTOCItems(survey: SurveyModel, onAction: () => void) {
}
},
visible: <any>new ComputedUpdater(() => {
return page.isVisible && !((<any>page)["isStartPage"]);
const isRootItem = level === 0;
const hasVisibleTitle = !page.isPanel || (page.showTitle && page.hasTitle);
return page.isVisible && (!isRootItem || !((<any>page)["isStartPage"])) && (isRootItem || hasVisibleTitle);
})
});
});
Expand Down Expand Up @@ -156,6 +192,7 @@ export class TOCModel {
public dispose(): void {
this.survey.unRegisterFunctionOnPropertyValueChanged("_isMobile", "toc");
const [handler] = this.survey.unRegisterFunctionOnPropertyValueChanged("pages", "toc");
this.survey.unRegisterFunctionOnPropertyValueChanged("panelsInTocLevel", "toc");
this.survey.onEndLoadingFromJson.remove(handler);
this.survey.onPageAdded.remove(handler);
this.popupModel.dispose();
Expand Down
59 changes: 59 additions & 0 deletions packages/survey-core/tests/surveyTOCTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,65 @@ QUnit.test("questionsOnPageMode singlePage selectedItem tracks focused question"
assert.equal(tocListModel.selectedItem.id, "page3", "3rd page is active after question3 focused");
});

QUnit.test("panelsInTocLevel includes nested titled panels up to configured depth", function (assert) {
const survey = new SurveyModel({
panelsInTocLevel: 2,
pages: [
{
name: "page1",
elements: [
{
type: "panel",
name: "panel1",
title: "Panel 1",
elements: [
{
type: "panel",
name: "panel11",
title: "Panel 1.1",
elements: [
{
type: "panel",
name: "panel111",
title: "Panel 1.1.1",
elements: [{ type: "text", name: "q111" }]
},
{ type: "text", name: "q11" }
]
},
{
type: "panel",
name: "panel12",
elements: [{ type: "text", name: "q12" }]
}
]
},
{
type: "panel",
name: "panel2",
elements: [{ type: "text", name: "q2" }]
},
{ type: "text", name: "q1" }
]
}
]
});

const tocListModel = createTOCListModel(survey);
assert.deepEqual(
tocListModel.visibleItems.map(item => item.id),
["page1", "panel1", "panel11"],
"Page and nested titled panels up to level 2 are included"
);

survey.panelsInTocLevel = 3;
assert.deepEqual(
tocListModel.visibleItems.map(item => item.id),
["page1", "panel1", "panel11", "panel111"],
"Level 3 includes deeper nested panels"
);
});

QUnit.test("respects markup", function (assert) {
let json: any = {
"pages": [
Expand Down
Loading