Skip to content

Commit ba2d5b9

Browse files
author
tsv2013
committed
Work for #6646 - Introduce an API to restrict questions and panels from being added or dropped into other container elements (e.g., other panels or dynamic panels) - Implemented maxNestingLevel option
1 parent ed0fce9 commit ba2d5b9

File tree

7 files changed

+189
-57
lines changed

7 files changed

+189
-57
lines changed

packages/survey-creator-core/src/creator-base.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1191,6 +1191,8 @@ export class SurveyCreatorModel extends Base
11911191
*/
11921192
public maxNestedPanels: number = -1;
11931193

1194+
public maxNestingLevel: number = -1;
1195+
11941196
public showPagesInTestSurveyTab = true;
11951197
/**
11961198
* Specifies whether to show a page selector at the bottom of the Preview tab.
@@ -2250,6 +2252,7 @@ export class SurveyCreatorModel extends Base
22502252
settings.dragDrop.restrictDragQuestionBetweenPages;
22512253
this.dragDropSurveyElements = new DragDropSurveyElements(null, this);
22522254
this.dragDropSurveyElements.onGetMaxNestedPanels = (): number => { return this.maxNestedPanels; };
2255+
this.dragDropSurveyElements.onGetMaxNestedLevel = (): number => { return this.maxNestingLevel; };
22532256
this.dragDropSurveyElements.onDragOverLocationCalculating = (options) => { this.onDragOverLocationCalculating.fire(this, options); };
22542257
let isDraggedFromToolbox = false;
22552258
this.dragDropSurveyElements.onDragStart.add((sender, options) => {
@@ -4155,10 +4158,16 @@ export class SurveyCreatorModel extends Base
41554158
const res: Array<QuestionToolboxItem> = [];
41564159
this.toolbox.items.forEach((item) => { if (!item.showInToolboxOnly) res.push(item); });
41574160

4158-
if (!element || this.maxNestedPanels < 0) return res;
4159-
if (!isAddNew && element.isPanel) return res;
4161+
if (!element) return res;
4162+
if (!isAddNew && (element.isPanel || SurveyHelper.isPanelDynamic(element))) return res;
41604163

4161-
if (this.maxNestedPanels < SurveyHelper.getElementDeepLength(element)) {
4164+
if (this.maxNestingLevel >= 0 && this.maxNestingLevel < SurveyHelper.getElementParentContainers(element).length) {
4165+
for (let i = res.length - 1; i >= 0; i--) {
4166+
if (res[i].isPanel || Serializer.isDescendantOf(res[i].typeName, "paneldynamic")) {
4167+
res.splice(i, 1);
4168+
}
4169+
}
4170+
} else if (this.maxNestedPanels >= 0 && this.maxNestedPanels < SurveyHelper.getElementDeepLength(element)) {
41624171
for (let i = res.length - 1; i >= 0; i--) {
41634172
if (res[i].isPanel) {
41644173
res.splice(i, 1);

packages/survey-creator-core/src/creator-options.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,9 @@ export interface ICreatorOptions {
295295
* Default value: -1 (unlimited)
296296
*/
297297
maxNestedPanels?: number;
298+
299+
maxNestingLevel?: number;
300+
298301
/**
299302
* @deprecated Survey Creator no longer supports switching between UI themes in the Preview tab.
300303
*/

packages/survey-creator-core/src/creator-settings.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,10 @@ export interface ISurveyCreatorOptions {
217217
maximumChoicesCount: number;
218218
maximumRowsCount: number;
219219
maximumRateValues: number;
220+
220221
maxNestedPanels: number;
222+
maxNestingLevel: number;
223+
221224
enableLinkFileEditor: boolean;
222225
inplaceEditForValues: boolean;
223226
rootElement?: HTMLElement;
@@ -367,7 +370,10 @@ export class EmptySurveyCreatorOptions implements ISurveyCreatorOptions {
367370
maximumRateValues: number = settings.propertyGrid.maximumRateValues;
368371
machineTranslationValue: boolean = false;
369372
inplaceEditForValues: boolean = false;
373+
370374
maxNestedPanels: number = -1;
375+
maxNestingLevel: number = -1;
376+
371377
showOneCategoryInPropertyGrid: boolean;
372378

373379
getObjectDisplayName(obj: Base, area: string, reason: string, displayName: string): string {

packages/survey-creator-core/src/dragdrop-survey-elements.ts

Lines changed: 16 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,10 @@ export class DragDropSurveyElements extends DragDropCore<any> {
8181
}
8282
protected isDraggedElementSelected: boolean = false;
8383
public onGetMaxNestedPanels: () => number;
84+
public onGetMaxNestedLevel: () => number;
8485
public onDragOverLocationCalculating: (options: any) => void;
8586
public get maxNestedPanels(): number { return this.onGetMaxNestedPanels ? this.onGetMaxNestedPanels() : -1; }
86-
87-
// private isRight: boolean;
88-
// protected prevIsRight: boolean;
87+
public get maxNestingLevel(): number { return this.onGetMaxNestedLevel ? this.onGetMaxNestedLevel() : -1; }
8988

9089
public startDragToolboxItem(
9190
event: PointerEvent,
@@ -162,10 +161,6 @@ export class DragDropSurveyElements extends DragDropCore<any> {
162161
return newElement;
163162
}
164163

165-
private isPanelDynamic(element: ISurveyElement) {
166-
return element instanceof QuestionPanelDynamicModel;
167-
}
168-
169164
protected findDropTargetNodeByDragOverNode(dragOverNode: HTMLElement): HTMLElement {
170165
const ghostRow = dragOverNode.closest(".svc-row--ghost");
171166
if (!!ghostRow) {
@@ -246,17 +241,21 @@ export class DragDropSurveyElements extends DragDropCore<any> {
246241
if (!dropTarget) return false;
247242
if (dropTarget === this.draggedElement) return false;
248243

249-
if (this.draggedElement.getType() === "paneldynamic" && dropTarget === this.draggedElement.template) {
244+
if (SurveyHelper.isPanelDynamic(this.draggedElement) && dropTarget === this.draggedElement.template) {
250245
return false;
251246
}
252-
if (this.maxNestedPanels >= 0 && this.draggedElement.isPanel) {
253-
const pnl: any = <PanelModel>this.draggedElement;
254-
if (pnl.deepNested === undefined) {
255-
pnl.deepNested = this.getMaximumNestedPanelCount(pnl, 0);
256-
}
247+
if (this.maxNestingLevel >= 0 && (this.draggedElement.isPanel || SurveyHelper.isPanelDynamic(this.draggedElement))) {
248+
const pnl: any = this.draggedElement as PanelModel;
249+
const childPanelsMaxNesting = SurveyHelper.getMaximumNestedPanelDepth(pnl, 0);
250+
let len = SurveyHelper.getElementParentContainers(dropTarget, false).length;
251+
if (dragOverLocation !== DropIndicatorPosition.Inside && dropTarget.isPanel) len--;
252+
if (this.maxNestingLevel < len + childPanelsMaxNesting) return false;
253+
} else if (this.maxNestedPanels >= 0 && this.draggedElement.isPanel) {
254+
const pnl: any = this.draggedElement as PanelModel;
255+
const childPanelsMaxNesting = SurveyHelper.getMaximumNestedPanelDepth(pnl, 0);
257256
let len = SurveyHelper.getElementDeepLength(dropTarget);
258257
if (dragOverLocation !== DropIndicatorPosition.Inside && dropTarget.isPanel) len--;
259-
if (this.maxNestedPanels < len + pnl.deepNested) return false;
258+
if (this.maxNestedPanels < len + childPanelsMaxNesting) return false;
260259
}
261260

262261
if (
@@ -268,18 +267,6 @@ export class DragDropSurveyElements extends DragDropCore<any> {
268267

269268
return true;
270269
}
271-
private getMaximumNestedPanelCount(panel: PanelModel, deep: number): number {
272-
let max = deep;
273-
panel.elements.forEach(el => {
274-
if (el.isPanel) {
275-
const pDeep = this.getMaximumNestedPanelCount(<PanelModel>el, deep + 1);
276-
if (pDeep > max) {
277-
max = pDeep;
278-
}
279-
}
280-
});
281-
return max;
282-
}
283270

284271
protected doBanDropHere = () => {
285272
this.removeDragOverMarker(this.dragOverIndicatorElement);
@@ -458,7 +445,7 @@ export class DragDropSurveyElements extends DragDropCore<any> {
458445
const calcDirection = !settings.dragDrop.allowDragToTheSameLine || (!!this.draggedElement && this.draggedElement.isPage) ? "top-bottom" : null;
459446
let dragOverLocation = calculateDragOverLocation(event.clientX, event.clientY, dropTargetRect, calcDirection);
460447

461-
if (!this.draggedElement.isPage && dropTarget && ((dropTarget.isPanel || dropTarget.isPage) && dropTarget.elements.length === 0 || this.isPanelDynamic(dropTarget) && dropTarget.template.elements.length == 0)) {
448+
if (!this.draggedElement.isPage && dropTarget && ((dropTarget.isPanel || dropTarget.isPage) && dropTarget.elements.length === 0 || SurveyHelper.isPanelDynamic(dropTarget) && dropTarget.template.elements.length == 0)) {
462449
if (dropTarget.isPage || this.insideElement) {
463450
dragOverLocation = DropIndicatorPosition.Inside;
464451
}
@@ -468,39 +455,14 @@ export class DragDropSurveyElements extends DragDropCore<any> {
468455
dragOverLocation = DropIndicatorPosition.Inside;
469456
}
470457

471-
if ((dropTarget.isPanel || this.isPanelDynamic(dropTarget)) && this.insideElement && dropTargetAdorner.collapsed) {
458+
if ((dropTarget.isPanel || SurveyHelper.isPanelDynamic(dropTarget)) && this.insideElement && dropTargetAdorner.collapsed) {
472459
dragOverLocation = DropIndicatorPosition.Inside;
473460
}
474461

475462
if (!this.draggedElement.isPage && dropTarget.isPage && dropTarget.elements.length !== 0 && !dropTargetAdorner.collapsed) {
476463
dragOverLocation = null;
477464
}
478465

479-
// const dropTargetType = this.getDragDropElementType(dropTarget);
480-
// const draggedElementType = this.getDragDropElementType(this.draggedElement);
481-
// switch (dropTargetType) {
482-
// case ElType.Page: {
483-
// console.log("dropTargetType", dropTargetType);
484-
// break;
485-
// }
486-
// case ElType.Panel: {
487-
// console.log("dropTargetType", dropTargetType);
488-
// break;
489-
// }
490-
// case ElType.DynamicPanel: {
491-
// console.log("dropTargetType", dropTargetType);
492-
// break;
493-
// }
494-
// case ElType.Question: {
495-
// console.log("dropTargetType", dropTargetType);
496-
// break;
497-
// }
498-
// // case ElType.EmptySurvey: {
499-
// // console.log("dropTargetType", dropTargetType);
500-
// // break;
501-
// // }
502-
// }
503-
504466
const options = {
505467
survey: this.survey,
506468
draggedSurveyElement: this.draggedElement,
@@ -591,7 +553,7 @@ export class DragDropSurveyElements extends DragDropCore<any> {
591553
let dest = this.dragOverIndicatorElement?.isPanel ? this.dragOverIndicatorElement : this.dropTarget;
592554

593555
if (this.dragOverLocation === DropIndicatorPosition.Inside) {
594-
if (this.isPanelDynamic(dest)) dest = dest.template;
556+
if (SurveyHelper.isPanelDynamic(dest)) dest = dest.template;
595557
(<PanelModelBase>dest).insertElement(src);
596558
} else {
597559
const destParent = dest.parent || dest.page;

packages/survey-creator-core/src/survey-helper.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
Serializer,
99
SurveyModel,
1010
PageModel,
11+
PanelModel,
1112
} from "survey-core";
1213
import { editorLocalization } from "./editorLocalization";
1314
import { ISurveyCreatorOptions } from "./creator-settings";
@@ -371,4 +372,30 @@ export class SurveyHelper {
371372
}
372373
return res;
373374
}
375+
public static getMaximumNestedPanelDepth(panel: PanelModel, currentDepth: number): number {
376+
let maxDepth = currentDepth;
377+
panel.elements.forEach(el => {
378+
if (el.isPanel) {
379+
const pDeep = SurveyHelper.getMaximumNestedPanelDepth(<PanelModel>el, currentDepth + 1);
380+
if (pDeep > maxDepth) {
381+
maxDepth = pDeep;
382+
}
383+
}
384+
});
385+
return maxDepth;
386+
}
387+
public static getElementParentContainers(element: SurveyElement, includingPage = true): SurveyElement[] {
388+
const containers: SurveyElement[] = [];
389+
let current = (element.parent || element.parentQuestion) as SurveyElement;
390+
while(!!current) {
391+
if (current.isInteractiveDesignElement && (!current.isPage || includingPage)) {
392+
containers.push(current);
393+
}
394+
current = (current.parent || current.parentQuestion) as SurveyElement;
395+
}
396+
return containers;
397+
}
398+
public static isPanelDynamic(element: any) {
399+
return !!element && (element as Base).isDescendantOf("paneldynamic");
400+
}
374401
}

packages/survey-creator-core/tests/creator-base-2.tests.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ test("json editor default indent", (): any => {
9393
expect(settings.jsonEditor.indentation).toBe(2);
9494
expect(creator.text).toBe("{\n \"pages\": [\n {\n \"name\": \"page1\"\n }\n ]\n}");
9595
});
96+
9697
test("onSetPropertyEditorOptions -> onConfigureTablePropertyEditor", (): any => {
9798
const creator = new CreatorTester();
9899
creator.JSON = { elements: [{ type: "dropdown", name: "q1", choices: [1, 2] }] };
@@ -123,6 +124,7 @@ test("onSetPropertyEditorOptions -> onConfigureTablePropertyEditor", (): any =>
123124
creator.onSetPropertyEditorOptionsCallback("choices", question, callBackOptions);
124125
expect(callBackOptions.allowBatchEdit).toBeFalsy();
125126
});
127+
126128
test("creator.onSurveyInstanceCreated from property Grid", () => {
127129
const creator = new CreatorTester();
128130
const selectedTypes = new Array<string>();
@@ -146,6 +148,7 @@ test("creator.onSurveyInstanceCreated from property Grid", () => {
146148
creator.selectQuestionByName("q2");
147149
expect(selectedTypes).toStrictEqual(["survey", "text", "radiogroup"]);
148150
});
151+
149152
test("creator.onSurveyInstanceSetupHandlers event", () => {
150153
const creator = new CreatorTester();
151154
let json = undefined;
@@ -189,6 +192,7 @@ test("check tabResponsivenessMode property", () => {
189192
creator.tabResponsivenessMode = "menu";
190193
expect(creator.tabbedMenu.actions.every((action) => action.disableShrink)).toBeTruthy();
191194
});
195+
192196
test("onModified options, on adding page and on copying page", () => {
193197
const creator = new CreatorTester();
194198
creator.JSON = {
@@ -480,6 +484,7 @@ test("creator set theme should update headerView survey property", (): any => {
480484
creator.theme = { headerView: "basic" };
481485
expect(creator.survey.headerView).toBe("basic");
482486
});
487+
483488
test("creator set theme should update headerView survey property", (): any => {
484489
const creator = new CreatorTester({
485490
clearTranslationsOnSourceTextChange: true
@@ -553,4 +558,83 @@ test("creator set theme should update headerView survey property", (): any => {
553558
expect(q2.locTitle.getJson()).toStrictEqual({
554559
de: "de: my question_new"
555560
});
561+
});
562+
563+
test("ConvertTo and addNewQuestion for panel with maxNestingLevel set", (): any => {
564+
const creator = new CreatorTester({ maxNestingLevel: 0 });
565+
creator.JSON = {
566+
elements: [
567+
{
568+
type: "panel", name: "panel1",
569+
elements: [
570+
{
571+
type: "panel", name: "panel3",
572+
elements: [
573+
{ type: "panel", name: "panel5" },
574+
{ type: "paneldynamic", name: "panel6" }
575+
]
576+
},
577+
{ type: "paneldynamic", name: "panel4" }
578+
]
579+
},
580+
{ type: "paneldynamic", name: "panel2" }
581+
]
582+
};
583+
expect(creator.maxNestedPanels).toBe(-1);
584+
expect(creator.maxNestingLevel).toBe(0);
585+
expect(creator.dragDropSurveyElements.maxNestedPanels).toBe(-1);
586+
expect(creator.dragDropSurveyElements.maxNestingLevel).toBe(0);
587+
creator.maxNestingLevel = -1;
588+
expect(creator.dragDropSurveyElements.maxNestingLevel).toBe(-1);
589+
const panel1 = creator.survey.getPanelByName("panel1");
590+
const panel2 = creator.survey.getQuestionByName("panel2");
591+
const panel3 = creator.survey.getPanelByName("panel3");
592+
const panel4 = creator.survey.getQuestionByName("panel4");
593+
const panel5 = creator.survey.getPanelByName("panel5");
594+
const panel6 = creator.survey.getQuestionByName("panel6");
595+
const itemCount = creator.getAvailableToolboxItems().length;
596+
expect(itemCount).toBe(21);
597+
const panel6Model = new QuestionAdornerViewModel(creator, panel6, undefined);
598+
const panel5Model = new QuestionAdornerViewModel(creator, panel5, undefined);
599+
expect(creator.getAvailableToolboxItems(panel5)).toHaveLength(itemCount);
600+
expect(creator.getAvailableToolboxItems(panel6)).toHaveLength(itemCount);
601+
creator.maxNestingLevel = 3;
602+
expect(creator.dragDropSurveyElements.maxNestingLevel).toBe(3);
603+
expect(creator.getAvailableToolboxItems(panel5)).toHaveLength(itemCount);
604+
expect(creator.getAvailableToolboxItems(panel6)).toHaveLength(itemCount);
605+
expect(panel6Model.getConvertToTypesActions()).toHaveLength(itemCount);
606+
expect(panel5Model.getConvertToTypesActions()).toHaveLength(itemCount);
607+
creator.maxNestingLevel = 2;
608+
expect(creator.dragDropSurveyElements.maxNestingLevel).toBe(2);
609+
expect(creator.getAvailableToolboxItems(panel5)).toHaveLength(itemCount - 2);
610+
expect(creator.getAvailableToolboxItems(panel6)).toHaveLength(itemCount - 2);
611+
expect(creator.getAvailableToolboxItems(panel3)).toHaveLength(itemCount);
612+
expect(creator.getAvailableToolboxItems(panel4)).toHaveLength(itemCount);
613+
expect(creator.getAvailableToolboxItems(panel2)).toHaveLength(itemCount);
614+
expect(creator.getAvailableToolboxItems(panel1)).toHaveLength(itemCount);
615+
expect(panel6Model.getConvertToTypesActions()).toHaveLength(itemCount);
616+
expect(panel5Model.getConvertToTypesActions()).toHaveLength(itemCount);
617+
expect(creator.getAvailableToolboxItems()).toHaveLength(itemCount);
618+
creator.maxNestingLevel = 1;
619+
expect(creator.dragDropSurveyElements.maxNestingLevel).toBe(1);
620+
expect(creator.getAvailableToolboxItems(panel5)).toHaveLength(itemCount - 2);
621+
expect(creator.getAvailableToolboxItems(panel6)).toHaveLength(itemCount - 2);
622+
expect(creator.getAvailableToolboxItems(panel3)).toHaveLength(itemCount - 2);
623+
expect(creator.getAvailableToolboxItems(panel4)).toHaveLength(itemCount - 2);
624+
expect(creator.getAvailableToolboxItems(panel2)).toHaveLength(itemCount);
625+
expect(creator.getAvailableToolboxItems(panel1)).toHaveLength(itemCount);
626+
expect(panel6Model.getConvertToTypesActions()).toHaveLength(itemCount);
627+
expect(panel5Model.getConvertToTypesActions()).toHaveLength(itemCount);
628+
expect(creator.getAvailableToolboxItems()).toHaveLength(itemCount);
629+
creator.maxNestingLevel = 0;
630+
expect(creator.dragDropSurveyElements.maxNestingLevel).toBe(0);
631+
expect(creator.getAvailableToolboxItems(panel5)).toHaveLength(itemCount - 2);
632+
expect(creator.getAvailableToolboxItems(panel6)).toHaveLength(itemCount - 2);
633+
expect(panel6Model.getConvertToTypesActions()).toHaveLength(itemCount);
634+
expect(panel5Model.getConvertToTypesActions()).toHaveLength(itemCount);
635+
expect(creator.getAvailableToolboxItems(panel3)).toHaveLength(itemCount - 2);
636+
expect(creator.getAvailableToolboxItems(panel4)).toHaveLength(itemCount - 2);
637+
expect(creator.getAvailableToolboxItems(panel1)).toHaveLength(itemCount - 2);
638+
expect(creator.getAvailableToolboxItems(panel2)).toHaveLength(itemCount - 2);
639+
expect(creator.getAvailableToolboxItems()).toHaveLength(itemCount);
556640
});

0 commit comments

Comments
 (0)