From d30f80fca09c9ef114cfb05589ca1a87ab59f6b2 Mon Sep 17 00:00:00 2001 From: OlgaLarina Date: Fri, 19 Jul 2024 14:19:47 +0300 Subject: [PATCH] Improve questions layouting (#8574) * work for #5178 Set the same width values to input fields when titleLocation is left * work for #8396 add unit tests * work for #8396 - calculate title width * work for #5178 Set the same width values to input fields when titleLocation is left * work for #5178 Set the same width values to input fields when titleLocation is left * work for #5178 Set the same width values to input fields when titleLocation is left * work for #5178 Set the same width values to input fields when titleLocation is left * work for #5178 fix unit tests * work for #5178 fix layoutColumns serialization * work for #5178 add a property with the actual value colSpan * work for #5178 fix reactivity * work for #5178 fix bugs * work for #5178 fix question style after added second question into row * work for #5178 fix panel width * work for #5178 fix markup tests * work for #5178 calculate colSpan if question invisible * work for #5178 rename isGridLayoutMode -> gridLayoutEnabled --------- Co-authored-by: OlgaLarina --- src/base-interfaces.ts | 6 + src/jsonobject.ts | 5 +- src/panel-layout-column.ts | 36 + src/panel.ts | 162 +++- src/question.ts | 94 ++- src/question_custom.ts | 5 + src/question_multipletext.ts | 5 + src/survey-element.ts | 55 +- src/survey.ts | 3 +- src/utils/utils.ts | 4 + tests/choicesRestfultests.ts | 7 +- tests/entries/test.ts | 1 + tests/layout_tests.ts | 717 ++++++++++++++++++ tests/markup/etalon.ts | 1 + tests/markup/etalon_layout.ts | 124 +++ tests/markup/etalon_page_panel.ts | 8 +- .../layout-page-swnl-title-v2.snap.html | 28 + .../snapshots/layout-page-swnl-v2.snap.html | 14 + .../layout-panel-page-swnl-title-v2.snap.html | 34 + .../layout-panel-page-swnl-v2.snap.html | 22 + 20 files changed, 1264 insertions(+), 67 deletions(-) create mode 100644 src/panel-layout-column.ts create mode 100644 tests/layout_tests.ts create mode 100644 tests/markup/etalon_layout.ts create mode 100644 tests/markup/snapshots/layout-page-swnl-title-v2.snap.html create mode 100644 tests/markup/snapshots/layout-page-swnl-v2.snap.html create mode 100644 tests/markup/snapshots/layout-panel-page-swnl-title-v2.snap.html create mode 100644 tests/markup/snapshots/layout-panel-page-swnl-v2.snap.html diff --git a/src/base-interfaces.ts b/src/base-interfaces.ts index 9eb49e958c..144e1ef7f4 100644 --- a/src/base-interfaces.ts +++ b/src/base-interfaces.ts @@ -9,6 +9,7 @@ import { SurveyError } from "./survey-error"; import { Base } from "./base"; import { IAction } from "./actions/action"; import { PanelModel } from "./panel"; +import { PanelLayoutColumnModel } from "./panel-layout-column"; import { QuestionPanelDynamicModel } from "./question_paneldynamic"; import { DragDropAllowEvent } from "./survey-events-api"; import { PopupModel } from "./popup"; @@ -96,6 +97,7 @@ export interface ISurvey extends ITextProcessor, ISurveyErrorOwner { value: any, displayValue: string ): string; + gridLayoutEnabled: boolean; isDisplayMode: boolean; isDesignMode: boolean; areInvisibleElementsShowing: boolean; @@ -282,6 +284,7 @@ export interface IElement extends IConditionRunner, ISurveyElement { isCollapsed: boolean; rightIndent: number; startWithNewLine: boolean; + colSpan?: number; registerPropertyChangedHandlers(propertyNames: Array, handler: any, key: string): void; registerFunctionOnPropertyValueChanged(name: string, func: any, key: string): void; unRegisterFunctionOnPropertyValueChanged(name: string, key: string): void; @@ -295,6 +298,7 @@ export interface IElement extends IConditionRunner, ISurveyElement { clearErrors(): any; dispose(): void; needResponsiveWidth(): boolean; + updateRootStyle(): void; } export interface IQuestion extends IElement, ISurveyErrorOwner { @@ -327,6 +331,8 @@ export interface IPanel extends ISurveyElement, IParentElement { getQuestionTitleWidth(): string; getQuestionStartIndex(): string; getQuestionErrorLocation(): string; + getColumsForElement(el: IElement): Array; + updateColumns(): void; parent: IPanel; elementWidthChanged(el: IElement): any; indexOf(el: IElement): number; diff --git a/src/jsonobject.ts b/src/jsonobject.ts index 61171c1a99..1f15a949c1 100644 --- a/src/jsonobject.ts +++ b/src/jsonobject.ts @@ -10,7 +10,7 @@ export interface IPropertyDecoratorOptions { localizable?: | { name?: string, onGetTextCallback?: (str: string) => string, defaultStr?: string } | boolean; - onSet?: (val: T, objectInstance: any) => void; + onSet?: (val: T, objectInstance: any, prevVal?: T) => void; } function ensureLocString( @@ -85,9 +85,10 @@ export function property(options: IPropertyDecoratorOptions = {}) { }, set: function (val: any) { const newValue = processComputedUpdater(this, val); + const prevValue = this.getPropertyValue(key); this.setPropertyValue(key, newValue); if (!!options && options.onSet) { - options.onSet(newValue, this); + options.onSet(newValue, this, prevValue); } }, }); diff --git a/src/panel-layout-column.ts b/src/panel-layout-column.ts new file mode 100644 index 0000000000..a81f47bc00 --- /dev/null +++ b/src/panel-layout-column.ts @@ -0,0 +1,36 @@ +import { property, Serializer } from "./jsonobject"; +import { Base } from "./base"; + +export class PanelLayoutColumnModel extends Base { + @property() width: number; + @property({ + onSet: (newValue, target, prevVal) => { + if (newValue !== prevVal) { + target.width = newValue; + } + } + }) effectiveWidth: number; + @property() questionTitleWidth: string; + + constructor(width?: number, questionTitleWidth?: string) { + super(); + this.effectiveWidth = width; + this.questionTitleWidth = questionTitleWidth; + } + + public getType(): string { + return "panellayoutcolumn"; + } + public isEmpty(): boolean { + return !this.width && !this.questionTitleWidth; + } +} + +Serializer.addClass("panellayoutcolumn", + [ + { name: "effectiveWidth:number", isSerializable: false, minValue: 0 }, + { name: "width:number", visible: false }, + "questionTitleWidth", + ], + (value: any) => new PanelLayoutColumnModel() +); \ No newline at end of file diff --git a/src/panel.ts b/src/panel.ts index fc94461214..5a2959a27a 100644 --- a/src/panel.ts +++ b/src/panel.ts @@ -1,6 +1,6 @@ -import { property, Serializer } from "./jsonobject"; +import { property, propertyArray, Serializer } from "./jsonobject"; import { HashTable, Helpers } from "./helpers"; -import { Base } from "./base"; +import { ArrayChanges, Base } from "./base"; import { ISurveyImpl, IPage, @@ -21,7 +21,7 @@ import { ElementFactory, QuestionFactory } from "./questionfactory"; import { LocalizableString } from "./localizablestring"; import { OneAnswerRequiredError } from "./error"; import { settings } from "./settings"; -import { findScrollableParent, getElementWidth, isElementVisible } from "./utils/utils"; +import { findScrollableParent, getElementWidth, isElementVisible, roundTo2Decimals } from "./utils/utils"; import { SurveyError } from "./survey-error"; import { CssClassBuilder } from "./utils/cssClassBuilder"; import { IAction } from "./actions/action"; @@ -32,6 +32,7 @@ import { DragDropInfo } from "./drag-drop-helper-v1"; import { AnimationGroup, IAnimationConsumer, IAnimationGroupConsumer } from "./utils/animation"; import { DomDocumentHelper, DomWindowHelper } from "./global_variables_utils"; import { PageModel } from "./page"; +import { PanelLayoutColumnModel } from "./panel-layout-column"; export class QuestionRowModel extends Base { private static rowCounter = 100; @@ -295,6 +296,11 @@ export class PanelModelBase extends SurveyElement private elementsValue: Array; private isQuestionsReady: boolean = false; private questionsValue: Array = new Array(); + private _columns: Array = undefined; + private _columnsReady = false; + + @propertyArray() layoutColumns: Array; + addElementCallback: (element: IElement) => void; removeElementCallback: (element: IElement) => void; onGetQuestionTitleLocation: () => string; @@ -314,7 +320,8 @@ export class PanelModelBase extends SurveyElement isAnimationEnabled: () => this.animationAllowed, getAnimatedElement: (row: QuestionRowModel) => row.getRootElement(), getLeaveOptions: (_: QuestionRowModel) => { - return { cssClass: this.cssClasses.rowFadeOut, + return { + cssClass: this.cssClasses.rowFadeOut, onBeforeRunAnimation: beforeRunAnimation }; }, @@ -397,6 +404,9 @@ export class PanelModelBase extends SurveyElement this.updateDescriptionVisibility(this.description); this.markQuestionListDirty(); this.onRowsChanged(); + this.layoutColumns.forEach(col => { + col.onPropertyValueChangedCallback = this.onColumnPropertyValueChangedCallback; + }); } @property({ defaultValue: true }) showTitle: boolean; @@ -1095,6 +1105,61 @@ export class PanelModelBase extends SurveyElement } } } + private calcMaxRowColSpan(): number { + let maxRowColSpan = 0; + this.rows.forEach(row => { + let curRowSpan = 0; + let userDefinedRow = false; + row.elements.forEach(el => { + if (!!el.width) { + userDefinedRow = true; + } + curRowSpan += (el.colSpan || 1); + }); + + if (!userDefinedRow && curRowSpan > maxRowColSpan) maxRowColSpan = curRowSpan; + }); + return maxRowColSpan; + } + private updateColumnWidth(columns: Array): void { + let remainingSpace = 0, remainingColCount = 0; + columns.forEach(col => { + if (!col.width) { + remainingColCount++; + } else { + remainingSpace += col.width; + col.setPropertyValue("effectiveWidth", col.width); + } + }); + if (!!remainingColCount) { + const oneColumnWidth = roundTo2Decimals((100 - remainingSpace) / remainingColCount); + for (let index = 0; index < columns.length; index++) { + if (!columns[index].width) { + columns[index].setPropertyValue("effectiveWidth", oneColumnWidth); + } + } + } + } + private onColumnPropertyValueChangedCallback = ( + name: string, + oldValue: any, + newValue: any, + sender: Base, + arrayChanges: ArrayChanges + ) => { + if (this._columnsReady) { + this.updateColumnWidth(this.layoutColumns); + this.updateRootStyle(); + } + } + public updateColumns() { + this._columns = undefined; + this.updateRootStyle(); + } + public updateRootStyle() { + super.updateRootStyle(); + this.elements?.forEach(el => el.updateRootStyle()); + } public updateCustomWidgets() { for (var i = 0; i < this.elements.length; i++) { this.elements[i].updateCustomWidgets(); @@ -1154,6 +1219,65 @@ export class PanelModelBase extends SurveyElement getQuestionTitleWidth(): string { return this.questionTitleWidth || this.parent && this.parent.getQuestionTitleWidth(); } + public get columns(): Array { + if (!this._columns) { + this.generateColumns(); + } + return this._columns || []; + } + protected generateColumns(): void { + let maxRowColSpan = this.calcMaxRowColSpan(); + let columns = [].concat(this.layoutColumns); + if (maxRowColSpan <= this.layoutColumns.length) { + columns = this.layoutColumns.slice(0, maxRowColSpan); + } else { + for (let index = this.layoutColumns.length; index < maxRowColSpan; index++) { + const newCol = new PanelLayoutColumnModel(); + newCol.onPropertyValueChangedCallback = this.onColumnPropertyValueChangedCallback; + columns.push(newCol); + } + } + this._columns = columns; + try { + this._columnsReady = false; + this.updateColumnWidth(columns); + } + finally { + this._columnsReady = true; + } + this.layoutColumns = columns; + } + public getColumsForElement(el: IElement): Array { + const row = this.findRowByElement(el); + if (!row || !this.survey || !this.survey.gridLayoutEnabled) return []; + + let lastExpandableElementIndex = row.elements.length - 1; + while (lastExpandableElementIndex >= 0) { + if (!(row.elements[lastExpandableElementIndex] as any).getPropertyValueWithoutDefault("colSpan")) { + break; + } + lastExpandableElementIndex--; + } + + const elementIndex = row.elements.indexOf(el); + let startIndex = 0; + for (let index = 0; index < elementIndex; index++) { + startIndex += row.elements[index].colSpan; + } + let currentColSpan = (el as any).getPropertyValueWithoutDefault("colSpan"); + if (!currentColSpan && elementIndex === lastExpandableElementIndex) { + let usedSpans = 0; + for (let index = 0; index < row.elements.length; index++) { + if (index !== lastExpandableElementIndex) { + usedSpans += row.elements[index].colSpan; + } + } + currentColSpan = this.columns.length - usedSpans; + } + const result = this.columns.slice(startIndex, startIndex + (currentColSpan || 1)); + (el as any).setPropertyValue("effectiveColSpan", result.length); + return result; + } protected getStartIndex(): string { if (!!this.parent) return this.parent.getQuestionStartIndex(); if (!!this.survey) return this.survey.questionStartIndex; @@ -1230,6 +1354,7 @@ export class PanelModelBase extends SurveyElement if (this.isLoadingFromJson) return; this.blockAnimations(); this.setArrayPropertyDirectly("rows", this.buildRows()); + this.updateColumns(); this.releaseAnimations(); } @@ -1409,10 +1534,8 @@ export class PanelModelBase extends SurveyElement } private updateRowsOnElementRemoved(element: IElement) { if (!this.canBuildRows()) return; - this.updateRowsRemoveElementFromRow( - element, - this.findRowByElement(element) - ); + this.updateRowsRemoveElementFromRow(element, this.findRowByElement(element)); + this.updateColumns(); } public updateRowsRemoveElementFromRow( element: IElement, @@ -1611,6 +1734,7 @@ export class PanelModelBase extends SurveyElement if(this.wasRendered) { element.onFirstRendering(); } + this.updateColumns(); return true; } public insertElement(element: IElement, dest?: IElement, location: "bottom" | "top" | "left" | "right" = "bottom"): void { @@ -1721,6 +1845,7 @@ export class PanelModelBase extends SurveyElement return false; } this.elements.splice(index, 1); + this.updateColumns(); return true; } public removeQuestion(question: Question) { @@ -1812,6 +1937,16 @@ export class PanelModelBase extends SurveyElement return new CssClassBuilder().append(cssClasses.error.root).toString(); } + public getSerializableColumnsValue(): Array { + let tailIndex = -1; + for (let index = this.layoutColumns.length - 1; index >= 0; index--) { + if (!this.layoutColumns[index].isEmpty()) { + tailIndex = index; + break; + } + } + return this.layoutColumns.slice(0, tailIndex + 1); + } public dispose(): void { super.dispose(); if (this.rows) { @@ -1845,6 +1980,7 @@ export class PanelModel extends PanelModelBase implements IElement { }); this.registerPropertyChangedHandlers( ["indent", "innerIndent", "rightIndent"], () => { this.onIndentChanged(); }); + this.registerPropertyChangedHandlers(["colSpan"], () => { this.parent?.updateColumns(); }); } public getType(): string { return "panel"; @@ -2202,6 +2338,11 @@ Serializer.addClass( default: "default", choices: ["default", "top", "bottom", "left", "hidden"], }, + { + name: "layoutColumns:panellayoutcolumns", + className: "panellayoutcolumn", isArray: true, + onSerializeValue: (obj: any): any => { return obj.getSerializableColumnsValue(); } + }, { name: "title:text", serializationProperty: "locTitle" }, { name: "description:text", serializationProperty: "locDescription" }, { @@ -2233,6 +2374,11 @@ Serializer.addClass( "width", { name: "minWidth", defaultFunc: () => "auto" }, { name: "maxWidth", defaultFunc: () => settings.maxWidth }, + { + name: "colSpan:number", visible: false, + onSerializeValue: (obj) => { return obj.getPropertyValue("colSpan"); }, + }, + { name: "effectiveColSpan:number", minValue: 1, isSerializable: false }, { name: "innerIndent:number", default: 0, choices: [0, 1, 2, 3] }, { name: "indent:number", default: 0, choices: [0, 1, 2, 3], visible: false }, { diff --git a/src/question.ts b/src/question.ts index 178cb620f4..d13f216ce7 100644 --- a/src/question.ts +++ b/src/question.ts @@ -12,7 +12,7 @@ import { QuestionCustomWidget } from "./questionCustomWidgets"; import { CustomWidgetCollection } from "./questionCustomWidgets"; import { settings } from "./settings"; import { SurveyModel } from "./survey"; -import { PanelModel } from "./panel"; +import { PanelModel, PanelModelBase } from "./panel"; import { RendererFactory } from "./rendererFactory"; import { SurveyError } from "./survey-error"; import { CssClassBuilder } from "./utils/cssClassBuilder"; @@ -46,7 +46,7 @@ export interface IQuestionPlainData { class TriggerExpressionInfo { runner: ExpressionRunner; isRunning: boolean; - constructor(public name: string, public canRun: () => boolean, public doComplete: () => void) {} + constructor(public name: string, public canRun: () => boolean, public doComplete: () => void) { } runSecondCheck: (keys: any) => boolean = (keys: any): boolean => false; } @@ -178,6 +178,7 @@ export class Question extends SurveyElement this.updateQuestionCss(); }); this.registerPropertyChangedHandlers(["isMobile"], () => { this.onMobileChanged(); }); + this.registerPropertyChangedHandlers(["colSpan"], () => { this.parent?.updateColumns(); }); } protected getDefaultTitle(): string { return this.name; } protected createLocTitleProperty(): LocalizableString { @@ -245,10 +246,10 @@ export class Question extends SurveyElement } protected updateIsReady(): void { let res = this.getIsQuestionReady(); - if(res) { + if (res) { const questions = this.getIsReadyDependsOn(); - for(let i = 0; i < questions.length; i ++) { - if(!questions[i].getIsQuestionReady()) { + for (let i = 0; i < questions.length; i++) { + if (!questions[i].getIsQuestionReady()) { res = false; break; } @@ -261,9 +262,9 @@ export class Question extends SurveyElement } private getAreNestedQuestionsReady(): boolean { const questions = this.getIsReadyNestedQuestions(); - if(!Array.isArray(questions)) return true; - for(let i = 0; i < questions.length; i ++) { - if(!questions[i].isReady) return false; + if (!Array.isArray(questions)) return true; + for (let i = 0; i < questions.length; i++) { + if (!questions[i].isReady) return false; } return true; } @@ -289,15 +290,15 @@ export class Question extends SurveyElement return this.getIsReadyDependendCore(false); } private getIsReadyDependendCore(isDependOn: boolean): Array { - if(!this.survey) return []; + if (!this.survey) return []; const questions = this.survey.questionsByValueName(this.getValueName()); const res = new Array(); - questions.forEach(q => { if(q !== this) res.push(q); }); - if(!isDependOn) { - if(this.parentQuestion) { + questions.forEach(q => { if (q !== this) res.push(q); }); + if (!isDependOn) { + if (this.parentQuestion) { res.push(this.parentQuestion); } - if(this.dependedQuestions.length > 0) { + if (this.dependedQuestions.length > 0) { this.dependedQuestions.forEach(q => res.push(q)); } } @@ -376,22 +377,22 @@ export class Question extends SurveyElement } protected onVisibleChanged(): void { this.updateIsVisibleProp(); - if (!this.isVisible &&this.errors && this.errors.length > 0) { + if (!this.isVisible && this.errors && this.errors.length > 0) { this.errors = []; } } protected notifyStateChanged(prevState: string): void { super.notifyStateChanged(prevState); - if(this.isCollapsed) { + if (this.isCollapsed) { this.onHidingContent(); } } private updateIsVisibleProp(): void { const prev = this.getPropertyValue("isVisible"); const val = this.isVisible; - if(prev !== val) { + if (prev !== val) { this.setPropertyValue("isVisible", val); - if(!val) { + if (!val) { this.onHidingContent(); } } @@ -461,7 +462,7 @@ export class Question extends SurveyElement public get visibleIndex(): number { return this.getPropertyValue("visibleIndex", -1); } - public onHidingContent(): void {} + public onHidingContent(): void { } /** * Hides the question number from the title and excludes the question from numbering. * @@ -529,7 +530,7 @@ export class Question extends SurveyElement } private setValueExpressionRunner: ExpressionRunner; private ensureSetValueExpressionRunner(): void { - if(!this.setValueExpressionRunner) { + if (!this.setValueExpressionRunner) { this.setValueExpressionRunner = new ExpressionRunner(this.setValueExpression); this.setValueExpressionRunner.onRunComplete = (res: any): void => { this.runExpressionSetValue(res); @@ -539,7 +540,7 @@ export class Question extends SurveyElement } } private runSetValueExpression(): void { - if(!this.setValueExpression) { + if (!this.setValueExpression) { this.clearValue(); } else { this.ensureSetValueExpressionRunner(); @@ -548,11 +549,11 @@ export class Question extends SurveyElement } private checkExpressionIf(keys: any): boolean { this.ensureSetValueExpressionRunner(); - if(!this.setValueExpressionRunner) return false; + if (!this.setValueExpressionRunner) return false; return new ProcessValue().isAnyKeyChanged(keys, this.setValueExpressionRunner.getVariables()); } private triggersInfo: Array = []; - private addTriggerInfo(name: string, canRun: ()=> boolean, doComplete: () => void): TriggerExpressionInfo { + private addTriggerInfo(name: string, canRun: () => boolean, doComplete: () => void): TriggerExpressionInfo { const info = new TriggerExpressionInfo(name, canRun, doComplete); this.triggersInfo.push(info); return info; @@ -561,16 +562,16 @@ export class Question extends SurveyElement const expression = this[info.name]; const keys: any = {}; keys[name] = value; - if(!expression || info.isRunning || !info.canRun()) { - if(info.runSecondCheck(keys)) { + if (!expression || info.isRunning || !info.canRun()) { + if (info.runSecondCheck(keys)) { info.doComplete(); } return; } - if(!info.runner) { + if (!info.runner) { info.runner = new ExpressionRunner(expression); info.runner.onRunComplete = (res: any): void => { - if(res === true) { + if (res === true) { info.doComplete(); } info.isRunning = false; @@ -578,12 +579,12 @@ export class Question extends SurveyElement } else { info.runner.expression = expression; } - if(!new ProcessValue().isAnyKeyChanged(keys, info.runner.getVariables()) && !info.runSecondCheck(keys)) return; + if (!new ProcessValue().isAnyKeyChanged(keys, info.runner.getVariables()) && !info.runSecondCheck(keys)) return; info.isRunning = true; info.runner.run(this.getDataFilteredValues(), this.getDataFilteredProperties()); } public runTriggers(name: string, value: any): void { - if(this.isSettingQuestionValue || (this.parentQuestion && this.parentQuestion.getValueName() === name)) return; + if (this.isSettingQuestionValue || (this.parentQuestion && this.parentQuestion.getValueName() === name)) return; this.triggersInfo.forEach(info => { this.runTriggerInfo(info, name, value); }); @@ -686,11 +687,25 @@ export class Question extends SurveyElement } public get titleWidth(): string { if (this.getTitleLocation() === "left") { - if (!!this.parent) { + const columns = this.parent.getColumsForElement(this as any); + const columnCount = columns.length; + if (columnCount !== 0 && !!columns[0].questionTitleWidth) return columns[0].questionTitleWidth; + + const percentWidth = this.getPercentQuestionTitleWidth(); + if (!percentWidth && !!this.parent) { let width = this.parent.getQuestionTitleWidth() as any; if (width && !isNaN(width)) width = width + "px"; return width; } + return (percentWidth / (columnCount || 1)) + "%"; + } + return undefined; + } + getPercentQuestionTitleWidth(): number { + const width = !!this.parent && this.parent.getQuestionTitleWidth(); + if (!!width && width[width.length - 1] === "%") { + return parseInt(width); + } return undefined; } @@ -1389,7 +1404,7 @@ export class Question extends SurveyElement protected onReadOnlyChanged(): void { this.setPropertyValue("isInputReadOnly", this.isInputReadOnly); super.onReadOnlyChanged(); - if(this.isReadOnly) { + if (this.isReadOnly) { this.clearErrors(); } this.updateQuestionCss(); @@ -2159,7 +2174,7 @@ export class Question extends SurveyElement return new CustomError(error, this.survey); } public removeError(error: SurveyError): void { - if(!error) return; + if (!error) return; var errors = this.errors; var index = errors.indexOf(error); if (index !== -1) errors.splice(index, 1); @@ -2208,9 +2223,9 @@ export class Question extends SurveyElement err.onUpdateErrorTextCallback = (err) => { err.text = this.requiredErrorText; }; errors.push(err); } - if(!this.isEmpty() && this.customWidget) { + if (!this.isEmpty() && this.customWidget) { const text = this.customWidget.validate(this); - if(!!text) { + if (!!text) { errors.push(this.addCustomError(text)); } } @@ -2268,7 +2283,7 @@ export class Question extends SurveyElement this.updateQuestionCss(); } this.isOldAnswered = undefined; - if(this.parent) { + if (this.parent) { this.parent.onQuestionValueChanged(this); } } @@ -2358,13 +2373,13 @@ export class Question extends SurveyElement newValue = this.valueFromDataCore(newValue); if (!this.checkIsValueCorrect(newValue)) return; const isEmpty = this.isValueEmpty(newValue); - if(!isEmpty && this.defaultValueExpression) { + if (!isEmpty && this.defaultValueExpression) { this.setDefaultValueCore((val: any): void => { this.updateValueFromSurveyCore(newValue, this.isTwoValueEquals(newValue, val)); }); } else { this.updateValueFromSurveyCore(newValue, this.data !== this.getSurvey()); - if(clearData && isEmpty) { + if (clearData && isEmpty) { this.isValueChangedDirectly = false; } } @@ -2382,7 +2397,7 @@ export class Question extends SurveyElement protected onChangeQuestionValue(newValue: any): void { } protected setValueChangedDirectly(val: boolean): void { this.isValueChangedDirectly = val; - if(!!this.setValueChangedDirectlyCallback) { + if (!!this.setValueChangedDirectlyCallback) { this.setValueChangedDirectlyCallback(val); } } @@ -2750,6 +2765,11 @@ Serializer.addClass("question", [ { name: "width" }, { name: "minWidth", defaultFunc: () => settings.minWidth }, { name: "maxWidth", defaultFunc: () => settings.maxWidth }, + { + name: "colSpan:number", visible: false, + onSerializeValue: (obj) => { return obj.getPropertyValue("colSpan"); }, + }, + { name: "effectiveColSpan:number", minValue: 1, isSerializable: false }, { name: "startWithNewLine:boolean", default: true, layout: "row" }, { name: "indent:number", default: 0, choices: [0, 1, 2, 3], layout: "row" }, { diff --git a/src/question_custom.ts b/src/question_custom.ts index 3f331731af..e29c8f7763 100644 --- a/src/question_custom.ts +++ b/src/question_custom.ts @@ -13,6 +13,7 @@ import { } from "./base-interfaces"; import { SurveyElement } from "./survey-element"; import { PanelModel } from "./panel"; +import { PanelLayoutColumnModel } from "./panel-layout-column"; import { Helpers, HashTable } from "./helpers"; import { ItemValue } from "./itemvalue"; import { QuestionTextProcessor } from "./textPreProcessor"; @@ -740,6 +741,10 @@ export abstract class QuestionCustomModelBase extends Question getQuestionTitleWidth(): string { return undefined; } + getColumsForElement(el: IElement): Array { + return []; + } + updateColumns() { } getQuestionStartIndex(): string { return this.getStartIndex(); } diff --git a/src/question_multipletext.ts b/src/question_multipletext.ts index b082f4ce2f..863fe28aec 100644 --- a/src/question_multipletext.ts +++ b/src/question_multipletext.ts @@ -21,6 +21,7 @@ import { HashTable, Helpers } from "./helpers"; import { CssClassBuilder } from "./utils/cssClassBuilder"; import { settings } from "./settings"; import { InputMaskBase } from "./mask/mask_base"; +import { PanelLayoutColumnModel } from "./panel-layout-column"; export interface IMultipleTextData extends ILocalizableOwner, IPanel { getSurvey(): ISurvey; @@ -813,6 +814,10 @@ export class QuestionMultipleTextModel extends Question getQuestionTitleWidth(): string { return undefined; } + getColumsForElement(el: IElement): Array { + return []; + } + updateColumns() { } getQuestionStartIndex(): string { return this.getStartIndex(); } diff --git a/src/survey-element.ts b/src/survey-element.ts index af39aa0f0b..c5907ac3fe 100644 --- a/src/survey-element.ts +++ b/src/survey-element.ts @@ -252,6 +252,20 @@ export class SurveyElement extends SurveyElementCore implements ISurvey public static CreateDisabledDesignElements: boolean = false; public disableDesignActions: boolean = SurveyElement.CreateDisabledDesignElements; + + @property({ + onSet: (newValue, target) => { + target.colSpan = newValue; + } + }) effectiveColSpan: number; + + public get colSpan(): number { + return this.getPropertyValue("colSpan", 1); + } + public set colSpan(val: number) { + this.setPropertyValue("colSpan", val); + } + constructor(name: string) { super(); this.name = name; @@ -260,6 +274,7 @@ export class SurveyElement extends SurveyElementCore implements ISurvey this.registerPropertyChangedHandlers(["isReadOnly"], () => { this.onReadOnlyChanged(); }); this.registerPropertyChangedHandlers(["errors"], () => { this.updateVisibleErrors(); }); this.registerPropertyChangedHandlers(["isSingleInRow"], () => { this.updateElementCss(false); }); + this.registerPropertyChangedHandlers(["minWidth", "maxWidth", "renderWidth", "allowRootStyle", "parent"], () => { this.updateRootStyle(); }); } protected onPropertyValueChanged(name: string, oldValue: any, newValue: any) { super.onPropertyValueChanged(name, oldValue, newValue); @@ -943,20 +958,35 @@ export class SurveyElement extends SurveyElementCore implements ISurvey } @property({ defaultValue: true }) allowRootStyle: boolean; + @property() rootStyle: any; - get rootStyle() { + public updateRootStyle(): void { let style: { [index: string]: any } = {}; - let minWidth = this.minWidth; - if (minWidth != "auto") minWidth = "min(100%, " + this.minWidth + ")"; - if (this.allowRootStyle && this.renderWidth) { - // style["width"] = this.renderWidth; - style["flexGrow"] = 1; - style["flexShrink"] = 1; - style["flexBasis"] = this.renderWidth; - style["minWidth"] = minWidth; - style["maxWidth"] = this.maxWidth; + let _width; + if (!!this.parent) { + const columns = this.parent.getColumsForElement(this as any); + _width = columns.reduce((sum, col) => col.effectiveWidth + sum, 0); + if (!!_width && _width !== 100) { + style["flexGrow"] = 0; + style["flexShrink"] = 0; + style["flexBasis"] = _width + "%"; + style["minWidth"] = undefined; + style["maxWidth"] = undefined; + } } - return style; + if (Object.keys(style).length == 0) { + let minWidth = this.minWidth; + if (minWidth != "auto") minWidth = "min(100%, " + minWidth + ")"; + if (this.allowRootStyle && this.renderWidth) { + // style["width"] = this.renderWidth; + style["flexGrow"] = 1; + style["flexShrink"] = 1; + style["flexBasis"] = this.renderWidth; + style["minWidth"] = minWidth; + style["maxWidth"] = this.maxWidth; + } + } + this.rootStyle = style; } private isContainsSelection(el: any) { let elementWithSelection: any = undefined; @@ -1086,7 +1116,8 @@ export class SurveyElement extends SurveyElementCore implements ISurvey }, getLeaveOptions: () => { const cssClasses = this.isPanel ? this.cssClasses.panel : this.cssClasses; - return { cssClass: cssClasses.contentFadeOut, + return { + cssClass: cssClasses.contentFadeOut, onBeforeRunAnimation: beforeRunAnimation, onAfterRunAnimation: afterRunAnimation }; diff --git a/src/survey.ts b/src/survey.ts index 6dedd72118..dc86c927c5 100644 --- a/src/survey.ts +++ b/src/survey.ts @@ -7102,7 +7102,7 @@ export class SurveyModel extends SurveyElementCore public set showTimerPanelMode(val: string) { this.setPropertyValue("showTimerPanelMode", val); } - + @property() gridLayoutEnabled: boolean; /** * Specifies how to calculate the survey width. * @@ -8064,6 +8064,7 @@ Serializer.addClass("survey", [ default: "auto", choices: ["auto", "static", "responsive"], }, + { name: "gridLayoutEnabled:boolean", default: false, visible: false }, { name: "width", visibleIf: (obj: any) => { return obj.widthMode === "static"; } }, { name: "fitToContainer:boolean", default: true, visible: false }, { name: "headerView", default: "basic", choices: ["basic", "advanced"], visible: false }, diff --git a/src/utils/utils.ts b/src/utils/utils.ts index c2e0cfd720..859d3db4ff 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -567,6 +567,10 @@ export function compareArrays(oldValue: Array, newValue: Array, getKey: return { reorderedItems, deletedItems, addedItems, mergedItems }; } +export function roundTo2Decimals(number: number): number { + return Math.round(number * 100) / 100; +} + export { mergeValues, getElementWidth, diff --git a/tests/choicesRestfultests.ts b/tests/choicesRestfultests.ts index 86bc9c8bfc..d445f42e9e 100644 --- a/tests/choicesRestfultests.ts +++ b/tests/choicesRestfultests.ts @@ -1621,6 +1621,7 @@ QUnit.test( function(assert) { var counter = 0; var survey = new SurveyModel(); + survey.gridLayoutEnabled = false; survey.onPropertyValueChangedCallback = function( name: string, oldValue: any, @@ -1642,11 +1643,7 @@ QUnit.test( }; counter = 0; survey.fromJSON(json); - assert.equal( - counter, - 0, - "We should call onPropertyValueChangedCallback on loading from JSON" - ); + assert.equal(counter, 0, "We should call onPropertyValueChangedCallback on loading from JSON"); var q = survey.getQuestionByName("q1"); q.choicesByUrl.url = "{state}{city}"; assert.equal(counter, 1, "call onPropertyValueChangedCallback this time"); diff --git a/tests/entries/test.ts b/tests/entries/test.ts index 7041fba98b..66e822a0a1 100644 --- a/tests/entries/test.ts +++ b/tests/entries/test.ts @@ -77,6 +77,7 @@ export * from "../mask/mask_settings_tests"; export * from "../mask/multipletext_mask_settings_tests"; export * from "../stylesManagerTests"; export * from "../headerTests"; +export * from "../layout_tests"; // localization import "../../src/localization/russian"; diff --git a/tests/layout_tests.ts b/tests/layout_tests.ts new file mode 100644 index 0000000000..740e12470a --- /dev/null +++ b/tests/layout_tests.ts @@ -0,0 +1,717 @@ +import { ArrayChanges, Base } from "../src/base"; +import { Serializer } from "../src/jsonobject"; +import { QuestionTextModel } from "../src/question_text"; +import { SurveyModel } from "../src/survey"; +import { roundTo2Decimals } from "../src/utils/utils"; + +export default QUnit.module("Layout:"); + +QUnit.test("columns generate - simple", function (assert) { + const surveyModel = new SurveyModel({ + gridLayoutEnabled: true, + elements: [{ + "name": "q1", + "type": "text", + colSpan: 1, + }, { + "name": "q2", + "type": "text", + colSpan: 2, + "startWithNewLine": false + }, + { + "name": "q3", + "type": "text", + }, + { + "name": "q4", + "type": "text", + "startWithNewLine": false + }] + }); + const page = surveyModel.pages[0]; + const q1 = surveyModel.getQuestionByName("q1"); + const q2 = surveyModel.getQuestionByName("q2"); + const q3 = surveyModel.getQuestionByName("q3"); + const q4 = surveyModel.getQuestionByName("q4"); + + assert.equal(page.rows.length, 2, "There are two rows"); + assert.equal(page.columns.length, 3); + assert.equal(page.columns[0].width, undefined); + assert.equal(roundTo2Decimals(page.columns[0].effectiveWidth), 33.33); + + assert.equal(page.getColumsForElement(q1).length, 1); + assert.deepEqual(page.getColumsForElement(q1), [page.columns[0]], "q1"); + assert.equal(page.getColumsForElement(q2).length, 2); + assert.deepEqual(page.getColumsForElement(q2), [page.columns[1], page.columns[1]], "q2"); + assert.equal(page.getColumsForElement(q3).length, 1); + assert.deepEqual(page.getColumsForElement(q3), [page.columns[0]], "q3"); + assert.equal(page.getColumsForElement(q4).length, 2); + assert.deepEqual(page.getColumsForElement(q4), [page.columns[1], page.columns[1]], "q4"); +}); + +QUnit.test("columns generate - complex", function (assert) { + const surveyModel = new SurveyModel({ + gridLayoutEnabled: true, + elements: [{ + "name": "q1", + "type": "text", + colSpan: 1, + }, { + "name": "q2", + "type": "text", + colSpan: 2, + "startWithNewLine": false + }, + { + "name": "q3", + "type": "text", + colSpan: 2, + }, + { + "name": "q4", + "type": "text", + "startWithNewLine": false + }] + }); + const page = surveyModel.pages[0]; + const q1 = surveyModel.getQuestionByName("q1"); + const q2 = surveyModel.getQuestionByName("q2"); + const q3 = surveyModel.getQuestionByName("q3"); + const q4 = surveyModel.getQuestionByName("q4"); + + assert.equal(page.rows.length, 2, "There are two rows"); + assert.equal(page.columns.length, 3); + assert.equal(page.columns[0].width, undefined); + assert.equal(roundTo2Decimals(page.columns[0].effectiveWidth), 33.33); + + assert.deepEqual(page.getColumsForElement(q1), [page.columns[0]]); + assert.deepEqual(page.getColumsForElement(q2), [page.columns[1], page.columns[1]]); + assert.deepEqual(page.getColumsForElement(q3), [page.columns[0], page.columns[1]]); + assert.deepEqual(page.getColumsForElement(q4), [page.columns[1]]); +}); + +QUnit.test("columns generate - title width", function (assert) { + const surveyModel = new SurveyModel({ + gridLayoutEnabled: true, + pages: [ + { + name: "page1", + questionTitleWidth: "25%", + elements: [{ + "name": "q1", + "type": "text", + colSpan: 1, + }, { + "name": "q2", + "type": "text", + colSpan: 2, + "startWithNewLine": false + }, + { + "name": "q3", + "type": "text", + colSpan: 2, + }, + { + "name": "q4", + "type": "text", + "startWithNewLine": false + }] + }] + }); + const page = surveyModel.pages[0]; + const q1 = surveyModel.getQuestionByName("q1"); + const q2 = surveyModel.getQuestionByName("q2"); + const q3 = surveyModel.getQuestionByName("q3"); + const q4 = surveyModel.getQuestionByName("q4"); + + assert.deepEqual(q1.titleWidth, undefined); + assert.deepEqual(q2.titleWidth, undefined); + assert.deepEqual(q3.titleWidth, undefined); + assert.deepEqual(q4.titleWidth, undefined); + + page.questionTitleLocation = "left"; + assert.deepEqual(q1.titleWidth, "25%"); + assert.deepEqual(q2.titleWidth, "12.5%"); + assert.deepEqual(q3.titleWidth, "12.5%"); + assert.deepEqual(q4.titleWidth, "25%"); +}); + +QUnit.test("user columns de/serialization", function (assert) { + const surveyModel = new SurveyModel({ + gridLayoutEnabled: true, + pages: [ + { + name: "page1", + layoutColumns: [ + { "width": 40, }, + { + "width": 45, + "questionTitleWidth": "200px" + }] + }] + }); + const page = surveyModel.pages[0]; + + assert.deepEqual(page.layoutColumns.length, 2); + assert.deepEqual(page.layoutColumns[0].width, 40); + assert.deepEqual(page.layoutColumns[0].questionTitleWidth, undefined); + assert.deepEqual(page.layoutColumns[1].width, 45); + assert.deepEqual(page.layoutColumns[1].questionTitleWidth, "200px"); + + page.layoutColumns[0].width = 70; + page.layoutColumns[0].questionTitleWidth = "300px"; + const result = surveyModel.toJSON(); + assert.deepEqual(result, { + gridLayoutEnabled: true, + pages: [ + { + name: "page1", + layoutColumns: [{ + "questionTitleWidth": "300px", + "width": 70, + }, { + "questionTitleWidth": "200px", + "width": 45, + }], + }] + }); +}); + +QUnit.test("layout columns de/serialization", function (assert) { + const json = { + gridLayoutEnabled: true, + pages: [ + { + name: "page1", + "elements": [ + { "type": "text", "name": "q1" }, + { + "type": "text", + "name": "q2", + "colSpan": 2, + "startWithNewLine": false + } + ], + }] + }; + const surveyModel = new SurveyModel(json); + const page = surveyModel.pages[0]; + assert.equal(page.layoutColumns.length, 3); + + const result = surveyModel.toJSON(); + assert.deepEqual(result, json); +}); + +QUnit.test("apply columns from layoutColumns #1", function (assert) { + const surveyModel = new SurveyModel({ + gridLayoutEnabled: true, + pages: [ + { + name: "page1", + layoutColumns: [ + { "width": 40 } + ], + elements: [ + { "name": "q1", "type": "text", }, + { "name": "q2", "type": "text", "startWithNewLine": false } + ] + }] + }); + const page = surveyModel.pages[0]; + const q1 = surveyModel.getQuestionByName("q1"); + const q2 = surveyModel.getQuestionByName("q2"); + + assert.deepEqual(page.columns.length, 2); + assert.deepEqual(page.columns[0].width, 40); + assert.deepEqual(page.columns[0].effectiveWidth, 40); + assert.deepEqual(page.columns[1].width, undefined); + assert.deepEqual(page.columns[1].effectiveWidth, 60); + + assert.equal(q1.rootStyle["flexBasis"], "40%"); + assert.equal(q2.rootStyle["flexBasis"], "60%"); +}); + +QUnit.test("apply columns from layoutColumns #2", function (assert) { + const surveyModel = new SurveyModel({ + gridLayoutEnabled: true, + pages: [ + { + name: "page1", + layoutColumns: [ + { "width": 20, }, + { "width": 30, }, + { "width": 25, }, + { "width": 25, } + ], + elements: [ + { "name": "q1", "type": "text", }, + { "name": "q2", "type": "text", "startWithNewLine": false }, + { + "name": "q3", + "type": "text", + "colSpan": 2, + "startWithNewLine": false + } + ] + }] + }); + const page = surveyModel.pages[0]; + const q1 = surveyModel.getQuestionByName("q1"); + const q2 = surveyModel.getQuestionByName("q2"); + const q3 = surveyModel.getQuestionByName("q3"); + + assert.deepEqual(page.columns.length, 4); + assert.deepEqual(page.columns[0].width, 20); + assert.deepEqual(page.columns[1].width, 30); + assert.deepEqual(page.columns[2].width, 25); + assert.deepEqual(page.columns[3].width, 25); + + assert.equal(q1.rootStyle["flexBasis"], "20%", "q1 rootStyle flexBasis"); + assert.equal(q2.rootStyle["flexBasis"], "30%", "q2 rootStyle flexBasis"); + assert.equal(q3.rootStyle["flexBasis"], "50%", "q3 rootStyle flexBasis"); +}); + +QUnit.test("apply columns from layoutColumns with given colSpan", function (assert) { + const surveyModel = new SurveyModel({ + gridLayoutEnabled: true, + pages: [ + { + name: "page1", + layoutColumns: [ + { "width": 20, }, + { "width": 30, }, + { "width": 25, }, + { "width": 25, } + ], + elements: [ + { "name": "q1", "type": "text", }, + { "name": "q2", "type": "text", "startWithNewLine": false }, + { + "name": "q3", + "type": "text", + "colSpan": 1, + "startWithNewLine": false + } + ] + }] + }); + const page = surveyModel.pages[0]; + const q1 = surveyModel.getQuestionByName("q1"); + const q2 = surveyModel.getQuestionByName("q2"); + const q3 = surveyModel.getQuestionByName("q3"); + + assert.deepEqual(page.columns.length, 3); + assert.deepEqual(page.columns[0].width, 20); + assert.deepEqual(page.columns[1].width, 30); + assert.deepEqual(page.columns[2].width, 25); + + assert.equal(q1.rootStyle["flexBasis"], "20%", "q1 rootStyle flexBasis"); + assert.equal(q2.rootStyle["flexBasis"], "30%", "q2 rootStyle flexBasis"); + assert.equal(q3.rootStyle["flexBasis"], "25%", "q3 rootStyle flexBasis"); +}); + +QUnit.test("check question width if column width is set for only one column", function (assert) { + const surveyModel = new SurveyModel({ + gridLayoutEnabled: true, + "pages": [ + { + "name": "page1", + "elements": [ + { "type": "text", "name": "q1" }, + { + "type": "text", + "name": "q2", + "colSpan": 2, + "startWithNewLine": false + } + ], + "layoutColumns": [ + { "width": 25 }, + {}, + {} + ] + } + ], + }); + const page = surveyModel.pages[0]; + const q1 = surveyModel.getQuestionByName("q1"); + const q2 = surveyModel.getQuestionByName("q2"); + + assert.deepEqual(page.columns.length, 3); + assert.deepEqual(page.columns[0].width, 25); + assert.deepEqual(page.columns[0].effectiveWidth, 25); + assert.deepEqual(page.columns[1].width, undefined); + assert.deepEqual(page.columns[1].effectiveWidth, 37.5); + assert.deepEqual(page.columns[2].width, undefined); + assert.deepEqual(page.columns[2].effectiveWidth, 37.5); + + assert.equal(q1.rootStyle["flexBasis"], "25%", "q1 rootStyle flexBasis"); + assert.equal(q2.rootStyle["flexBasis"], "75%", "q2 rootStyle flexBasis"); +}); + +QUnit.test("effectiveColSpan #1", assert => { + const surveyModel = new SurveyModel({ + gridLayoutEnabled: true, + "questions": [ + { "type": "text", "name": "q1" }, + { + "type": "text", + "name": "q2", + "colSpan": 2, + "startWithNewLine": false + }, + { "type": "text", "name": "q3" }, + { + "type": "text", + "name": "q4", + "startWithNewLine": false + } + ] + }); + + const q3 = surveyModel.getQuestionByName("q3"); + const q4 = surveyModel.getQuestionByName("q4"); + + assert.equal(q3.colSpan, 1, "q3 colSpan #1"); + assert.equal(q3.effectiveColSpan, 1, "q3 effectiveColSpan #1"); + assert.equal(q4.colSpan, 1, "q4 colSpan #1"); + assert.equal(q4.effectiveColSpan, 2, "q4 effectiveColSpan #1"); + + q3.effectiveColSpan = 2; + assert.equal(q3.colSpan, 2, "q3 colSpan #2"); + assert.equal(q3.effectiveColSpan, 2, "q3 effectiveColSpan #2"); + assert.equal(q4.colSpan, 1, "q4 colSpan #2"); + assert.equal(q4.effectiveColSpan, 1, "q4 effectiveColSpan #2"); +}); + +QUnit.test("columns effectiveWidth #1", assert => { + const surveyModel = new SurveyModel({ + gridLayoutEnabled: true, + "pages": [ + { + "name": "page1", + "elements": [ + { "type": "text", "name": "q1" }, + { + "type": "text", + "name": "q2", + "colSpan": 2, + "startWithNewLine": false + }, + { "type": "text", "name": "q5", "startWithNewLine": false }, + { "type": "text", "name": "q3" }, + { "type": "text", "name": "q4", "startWithNewLine": false } + ], + "layoutColumns": [ + { "width": 25 }, + { "width": 30 }, + { "width": 35 } + ] + } + ] + }); + + const page = surveyModel.pages[0]; + + assert.equal(page.layoutColumns.length, 4); + assert.equal(page.layoutColumns[0].effectiveWidth, 25); + assert.equal(page.layoutColumns[1].effectiveWidth, 30); + assert.equal(page.layoutColumns[2].effectiveWidth, 35); + assert.equal(page.layoutColumns[3].effectiveWidth, 10); + assert.equal(page.layoutColumns[3].width, undefined); + + page.layoutColumns[0].effectiveWidth = 20; + assert.equal(page.layoutColumns[0].effectiveWidth, 20); + assert.equal(page.layoutColumns[0].width, 20); + assert.equal(page.layoutColumns[1].effectiveWidth, 30); + assert.equal(page.layoutColumns[2].effectiveWidth, 35); + assert.equal(page.layoutColumns[3].effectiveWidth, 15); + assert.equal(page.layoutColumns[3].width, undefined); +}); + +QUnit.test("colSpan for first row", assert => { + const surveyModel = new SurveyModel({ + gridLayoutEnabled: true, + "pages": [ + { + "name": "page1", + "elements": [ + { "type": "text", "name": "q4" }, + { "type": "text", "name": "q1" }, + { + "type": "text", "name": "q2", + "colSpan": 2, "startWithNewLine": false + }, + { "type": "text", "name": "q3" } + ] + } + ] + }); + + const q1 = surveyModel.getQuestionByName("q1"); + const q2 = surveyModel.getQuestionByName("q2"); + const q3 = surveyModel.getQuestionByName("q3"); + const q4 = surveyModel.getQuestionByName("q4"); + const page = surveyModel.pages[0]; + + assert.equal(page.layoutColumns.length, 3); + assert.equal(q1.effectiveColSpan, 1, "q1 effectiveColSpan"); + assert.equal(q2.effectiveColSpan, 2, "q2 effectiveColSpan"); + assert.equal(q3.effectiveColSpan, 3, "q3 effectiveColSpan"); + assert.equal(q4.effectiveColSpan, 3, "q4 effectiveColSpan"); +}); + +QUnit.test("expand last question in row whitch does not have colSpan set", assert => { + const surveyModel = new SurveyModel({ + gridLayoutEnabled: true, + "pages": [ + { + "name": "page1", + "elements": [ + { "type": "text", "name": "question1" }, + { "type": "text", "name": "question2", "startWithNewLine": false }, + { "type": "text", "name": "question3", "startWithNewLine": false }, + { "type": "text", "name": "question4", "startWithNewLine": false }, + { "type": "text", "name": "question5", "startWithNewLine": false }, + { "type": "text", "name": "question9" }, + { + "type": "text", + "name": "question10", + "colSpan": 2, + "startWithNewLine": false + } + ] + } + ] + }); + + const q9 = surveyModel.getQuestionByName("question9"); + const q10 = surveyModel.getQuestionByName("question10"); + const page = surveyModel.pages[0]; + + assert.equal(page.layoutColumns.length, 5); + assert.equal(q9.effectiveColSpan, 3, "q9 effectiveColSpan"); + assert.equal(q10.effectiveColSpan, 2, "q10 effectiveColSpan"); +}); + +QUnit.test("recalculate column width after question added", assert => { + const surveyModel = new SurveyModel({ + gridLayoutEnabled: true, + "pages": [ + { + "name": "page1", + "elements": [ + { "type": "text", "name": "question1" }, + { "type": "text", "name": "question2", "startWithNewLine": false } + ] + } + ] + }); + + const page = surveyModel.pages[0]; + + assert.equal(page.layoutColumns.length, 2); + assert.equal(page.layoutColumns[0].effectiveWidth, 50); + assert.equal(page.layoutColumns[1].effectiveWidth, 50); + + const q3 = new QuestionTextModel("q3"); + q3.startWithNewLine = false; + page.addElement(q3, 2); + assert.equal(page.layoutColumns.length, 3); + assert.equal(page.layoutColumns[0].effectiveWidth, 33.33); + assert.equal(page.layoutColumns[1].effectiveWidth, 33.33); + assert.equal(page.layoutColumns[2].effectiveWidth, 33.33); +}); + +QUnit.test("recalculate column width after question deleted", assert => { + const surveyModel = new SurveyModel({ + gridLayoutEnabled: true, + "pages": [ + { + "name": "page1", + "layoutColumns": [ + { "width": 51 }, + { "width": 47 } + ], + "elements": [ + { "type": "text", "name": "q1" }, + { "type": "text", "name": "q2", "startWithNewLine": false } + ] + } + ] + }); + + const page = surveyModel.pages[0]; + assert.equal(page.layoutColumns.length, 2); + assert.equal(page.layoutColumns[0].effectiveWidth, 51); + assert.equal(page.layoutColumns[1].effectiveWidth, 47); + + const q3 = surveyModel.getQuestionByName("q2"); + q3.delete(); + + assert.equal(page.layoutColumns.length, 1); + assert.equal(page.layoutColumns[0].effectiveWidth, 51); +}); + +QUnit.test("question root style", function (assert) { + const surveyModel = new SurveyModel({ + gridLayoutEnabled: true, + pages: [ + { + name: "page1", + elements: [{ "name": "q1", "type": "text" }] + } + ] + }); + const page = surveyModel.pages[0]; + const q1 = surveyModel.getQuestionByName("q1"); + + assert.deepEqual(q1.rootStyle, { + "flexBasis": "100%", + "flexGrow": 1, + "flexShrink": 1, + "maxWidth": "100%", + "minWidth": "min(100%, 300px)" + }); + + const q2 = new QuestionTextModel("q2"); + q2.startWithNewLine = false; + page.addElement(q2, 1); + + assert.deepEqual(q1.rootStyle, { + "flexBasis": "50%", + "flexGrow": 0, + "flexShrink": 0, + "maxWidth": undefined, + "minWidth": undefined + }); + + assert.deepEqual(q2.rootStyle, { + "flexBasis": "50%", + "flexGrow": 0, + "flexShrink": 0, + "maxWidth": undefined, + "minWidth": undefined + }); +}); + +QUnit.test("layoutColumns: serialize last column", assert => { + const json = { + gridLayoutEnabled: true, + "pages": [ + { + "name": "page1", + "elements": [ + { "type": "text", "name": "question1" }, + { "type": "text", "name": "question3", "startWithNewLine": false }, + { "type": "text", "name": "question2", "startWithNewLine": false }, + { "type": "text", "name": "question4" }, + { "type": "text", "name": "question7", "startWithNewLine": false }, + { "type": "text", "name": "question8", "startWithNewLine": false }, + { "type": "text", "name": "question9", "startWithNewLine": false } + ], + "layoutColumns": [ + { "width": 10 } + ] + } + ], + }; + const surveyModel = new SurveyModel(json); + const page = surveyModel.pages[0]; + + assert.deepEqual(page.layoutColumns.length, 4); + assert.deepEqual(page.layoutColumns[0].width, 10); + assert.deepEqual(page.layoutColumns[1].effectiveWidth, 30); + assert.deepEqual(page.layoutColumns[2].effectiveWidth, 30); + assert.deepEqual(page.layoutColumns[3].effectiveWidth, 30); + + page.layoutColumns[3].width = 10; + const result = surveyModel.toJSON(); + assert.deepEqual(result["pages"][0]["layoutColumns"], [ + { "width": 10 }, + {}, + {}, + { "width": 10 } + ]); +}); + +QUnit.test("layoutColumns: questions with visibleIf", assert => { + const json = { + gridLayoutEnabled: true, + "questions": [ + { "type": "text", "name": "q1" }, + { + "type": "radiogroup", + "name": "q2", + "startWithNewLine": false, + "choices": ["Yes", "No"], + "colCount": 0 + }, + { + "type": "text", + "name": "q3", + "visibleIf": "{q2} = 'Yes'", + "startWithNewLine": false, + }, + { + "type": "text", + "name": "q4", + "visibleIf": "{q2} = 'No'", + "startWithNewLine": false, + }, + { + "type": "text", + "name": "q5", + "visibleIf": "{q2} = 'No'", + "startWithNewLine": false, + }, + { + "type": "text", + "name": "q6", + "visibleIf": "{q2} = 'No' and {q5} = 'Yes'", + "startWithNewLine": false, + } + ] + }; + const surveyModel = new SurveyModel(json); + const page = surveyModel.pages[0]; + const q1 = surveyModel.getQuestionByName("q1"); + const q2 = surveyModel.getQuestionByName("q2"); + const q3 = surveyModel.getQuestionByName("q3"); + const q4 = surveyModel.getQuestionByName("q4"); + const q5 = surveyModel.getQuestionByName("q5"); + const q6 = surveyModel.getQuestionByName("q6"); + + assert.deepEqual(page.layoutColumns.length, 6); + assert.equal(q1.visible, true, "q1 visible"); + assert.equal(q2.visible, true, "q2 visible"); + assert.equal(q3.visible, false, "q3 visible"); + assert.equal(q4.visible, false, "q4 visible"); + assert.equal(q5.visible, false, "q5 visible"); + assert.equal(q6.visible, false, "q6 visible"); + + assert.equal(q1.effectiveColSpan, 1, "q1 effectiveColSpan"); + assert.equal(q2.effectiveColSpan, 1, "q2 effectiveColSpan"); + + q2.value = "No"; + assert.equal(q1.visible, true, "q1 visible"); + assert.equal(q2.visible, true, "q2 visible"); + assert.equal(q3.visible, false, "q3 visible"); + assert.equal(q4.visible, true, "q4 visible"); + assert.equal(q5.visible, true, "q5 visible"); + assert.equal(q6.visible, false, "q6 visible"); +}); + +QUnit.skip("Do not call survey.onPropertyValueChangedCallback on loading choicesByUrl, Bug#2563", function (assert) { + let counter = 0; + let survey = new SurveyModel({ gridLayoutEnabled: true }); + survey.onPropertyValueChangedCallback = function (name: string, oldValue: any, newValue: any, sender: Base, arrayChanges: ArrayChanges) { + if (!Serializer.findProperty(sender.getType(), name)) return; + counter++; + }; + counter = 0; + survey.fromJSON({ elements: [{ type: "text", name: "q1" }] }); + assert.equal(counter, 0, "We shouldn't call onPropertyValueChangedCallback on loading from JSON"); +}); \ No newline at end of file diff --git a/tests/markup/etalon.ts b/tests/markup/etalon.ts index 53efa278cd..deeec65360 100644 --- a/tests/markup/etalon.ts +++ b/tests/markup/etalon.ts @@ -22,6 +22,7 @@ export * from "./etalon_question"; export * from "./etalon_survey"; export * from "./etalon_signaturepad"; export * from "./etalon_expression"; +export * from "./etalon_layout"; export { markupTests } from "./helper"; registerMarkupTests([ diff --git a/tests/markup/etalon_layout.ts b/tests/markup/etalon_layout.ts new file mode 100644 index 0000000000..4ab6b9a478 --- /dev/null +++ b/tests/markup/etalon_layout.ts @@ -0,0 +1,124 @@ +import { StylesManager } from "survey-core"; +import { registerMarkupTest } from "./helper"; + +registerMarkupTest( + { + name: "Layout mode - start with new line with panel", + json: { + gridLayoutEnabled: true, + "elements": [ + { + type: "html", + name: "question0", + html: "HTML1", + title: "Question title", + titleLocation: "hidden" + }, + { + type: "panel", + name: "name", + showQuestionNumbers: "off", + startWithNewLine: false, + elements: [ + { + type: "html", + name: "question1", + html: "HTML2", + title: "Question title", + titleLocation: "hidden" + } + ] + } + ] + }, + before: () => StylesManager.applyTheme("defaultV2"), + after: () => StylesManager.applyTheme("default"), + event: "onAfterRenderPage", + snapshot: "layout-panel-page-swnl-v2", + }); + +registerMarkupTest( + { + name: "Layout mode - start with new line", + json: { + gridLayoutEnabled: true, + "elements": [ + { + type: "html", + name: "question0", + html: "HTML1", + title: "Question title", + titleLocation: "hidden" + }, + { + type: "html", + name: "question1", + html: "HTML2", + title: "Question title", + titleLocation: "hidden", + startWithNewLine: false + } + ] + }, + before: () => StylesManager.applyTheme("defaultV2"), + after: () => StylesManager.applyTheme("default"), + event: "onAfterRenderPage", + snapshot: "layout-page-swnl-v2", + }); + +registerMarkupTest( + { + name: "Layout mode - start with new line with panel and titles", + json: { + gridLayoutEnabled: true, + "elements": [ + { + type: "text", + name: "question0", + title: "Question title", + }, + { + type: "panel", + name: "name", + showQuestionNumbers: "off", + startWithNewLine: false, + elements: [ + { + type: "text", + name: "question1", + title: "Question title", + } + ] + } + ] + }, + before: () => StylesManager.applyTheme("defaultV2"), + after: () => StylesManager.applyTheme("default"), + event: "onAfterRenderPage", + snapshot: "layout-panel-page-swnl-title-v2", + }); + +registerMarkupTest( + { + name: "Layout mode - start with new line Title", + json: { + gridLayoutEnabled: true, + "elements": [ + { + type: "text", + name: "question0", + title: "Question title" + }, + { + type: "text", + name: "question1", + title: "Question title", + startWithNewLine: false + } + ] + }, + before: () => StylesManager.applyTheme("defaultV2"), + after: () => StylesManager.applyTheme("default"), + event: "onAfterRenderPage", + snapshot: "layout-page-swnl-title-v2", + }); \ No newline at end of file diff --git a/tests/markup/etalon_page_panel.ts b/tests/markup/etalon_page_panel.ts index 56f46100a7..82cf1aee17 100644 --- a/tests/markup/etalon_page_panel.ts +++ b/tests/markup/etalon_page_panel.ts @@ -52,7 +52,7 @@ registerMarkupTest( registerMarkupTest( { - name: "Test Panel - start with new line", + name: "Test Panel - panel start with new line", json: { "elements": [ { @@ -89,6 +89,7 @@ registerMarkupTest( { name: "Test Page - start with new line with panel", json: { + gridLayoutEnabled: false, "elements": [ { type: "html", @@ -124,6 +125,7 @@ registerMarkupTest( { name: "Test Page - start with new line", json: { + gridLayoutEnabled: false, "elements": [ { type: "html", @@ -152,6 +154,7 @@ registerMarkupTest( { name: "Test Page - start with new line with panel and titles", json: { + gridLayoutEnabled: false, "elements": [ { type: "text", @@ -200,6 +203,7 @@ registerMarkupTest( { name: "Test Page - start with new line Title", json: { + gridLayoutEnabled: false, "elements": [ { type: "text", @@ -221,7 +225,7 @@ registerMarkupTest( }); registerMarkupTest({ - name: "Test Page - start with new line Title", + name: "Test Page - questionTitleWidth", json: { "pages": [ { diff --git a/tests/markup/snapshots/layout-page-swnl-title-v2.snap.html b/tests/markup/snapshots/layout-page-swnl-title-v2.snap.html new file mode 100644 index 0000000000..f19d65c710 --- /dev/null +++ b/tests/markup/snapshots/layout-page-swnl-title-v2.snap.html @@ -0,0 +1,28 @@ +
+
+
+
+ +   + Question title +
+
+ +
+
+
+
+
+
+ +   + Question title +
+
+ +
+
\ No newline at end of file diff --git a/tests/markup/snapshots/layout-page-swnl-v2.snap.html b/tests/markup/snapshots/layout-page-swnl-v2.snap.html new file mode 100644 index 0000000000..43c6cc0c9d --- /dev/null +++ b/tests/markup/snapshots/layout-page-swnl-v2.snap.html @@ -0,0 +1,14 @@ +
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/tests/markup/snapshots/layout-panel-page-swnl-title-v2.snap.html b/tests/markup/snapshots/layout-panel-page-swnl-title-v2.snap.html new file mode 100644 index 0000000000..bcc5f6530d --- /dev/null +++ b/tests/markup/snapshots/layout-panel-page-swnl-title-v2.snap.html @@ -0,0 +1,34 @@ +
+
+
+
+ +   + Question title +
+
+ +
+
+
+
+
+
+
+
+
+
+ Question title +
+
+ +
+
+
+
+
+
\ No newline at end of file diff --git a/tests/markup/snapshots/layout-panel-page-swnl-v2.snap.html b/tests/markup/snapshots/layout-panel-page-swnl-v2.snap.html new file mode 100644 index 0000000000..8c6abce03c --- /dev/null +++ b/tests/markup/snapshots/layout-panel-page-swnl-v2.snap.html @@ -0,0 +1,22 @@ +
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
\ No newline at end of file